@ugurdemirel/landcraft 0.1.4 → 0.1.5
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/dist/styles.css +12 -0
- package/package.json +11 -4
- package/src/components/Badge.test.tsx +55 -0
- package/src/components/Blog/Blog.test.tsx +64 -0
- package/src/components/Button.test.tsx +91 -0
- package/src/components/CTA/CTA.test.tsx +53 -0
- package/src/components/Card.test.tsx +49 -0
- package/src/components/Container.test.tsx +52 -0
- package/src/components/FAQ/FAQ.test.tsx +54 -0
- package/src/components/Features/Features.test.tsx +49 -0
- package/src/components/Footer/Footer.test.tsx +49 -0
- package/src/components/Hero/Hero.test.tsx +71 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.test.tsx +64 -0
- package/src/components/LogoCloud/LogoCloud.test.tsx +35 -0
- package/src/components/MegaMenu/MegaMenu.test.tsx +89 -0
- package/src/components/Modal.test.tsx +80 -0
- package/src/components/Navbar/Navbar.test.tsx +91 -0
- package/src/components/Newsletter/Newsletter.test.tsx +45 -0
- package/src/components/Pricing/Pricing.test.tsx +67 -0
- package/src/components/Prose.test.tsx +49 -0
- package/src/components/Section.test.tsx +51 -0
- package/src/components/Slot.test.tsx +45 -0
- package/src/components/Stats/Stats.test.tsx +47 -0
- package/src/components/Testimonials/Testimonials.test.tsx +48 -0
- package/src/test/setup.ts +7 -0
- package/src/utils/cn.test.ts +21 -0
- package/src/utils/contrast.test.ts +71 -0
- package/src/utils/iso2Flag.test.ts +26 -0
- package/src/utils/useTokenForeground.test.ts +50 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { LanguageSwitcher } from "./LanguageSwitcher";
|
|
4
|
+
|
|
5
|
+
const languages = [
|
|
6
|
+
{ code: "tr", label: "Türkçe" },
|
|
7
|
+
{ code: "en", label: "English" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function trigger(container: HTMLElement): HTMLButtonElement {
|
|
11
|
+
// The dropdown trigger is the only button with aria-haspopup="listbox".
|
|
12
|
+
return container.querySelector('button[aria-haspopup="listbox"]')!;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("LanguageSwitcher", () => {
|
|
16
|
+
it("renders a trigger showing the current language (dropdown by default)", () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<LanguageSwitcher languages={languages} defaultValue="tr" />,
|
|
19
|
+
);
|
|
20
|
+
expect(trigger(container)).toBeInTheDocument();
|
|
21
|
+
expect(trigger(container)).toHaveTextContent("Türkçe");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("opens a listbox and selects a language via onChange", () => {
|
|
25
|
+
const onChange = vi.fn();
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<LanguageSwitcher languages={languages} defaultValue="en" onChange={onChange} />,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
fireEvent.click(trigger(container));
|
|
31
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
fireEvent.click(screen.getByRole("option", { name: /Türkçe/ }));
|
|
34
|
+
expect(onChange).toHaveBeenCalledWith("tr");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("marks the active option as selected", () => {
|
|
38
|
+
const { container } = render(
|
|
39
|
+
<LanguageSwitcher languages={languages} defaultValue="en" />,
|
|
40
|
+
);
|
|
41
|
+
fireEvent.click(trigger(container));
|
|
42
|
+
|
|
43
|
+
const enOption = screen.getByRole("option", { name: /English/ });
|
|
44
|
+
expect(enOption).toHaveAttribute("aria-selected", "true");
|
|
45
|
+
const trOption = screen.getByRole("option", { name: /Türkçe/ });
|
|
46
|
+
expect(trOption).toHaveAttribute("aria-selected", "false");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("collapses the dropdown after selecting a language", () => {
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<LanguageSwitcher languages={languages} defaultValue="en" />,
|
|
52
|
+
);
|
|
53
|
+
fireEvent.click(trigger(container));
|
|
54
|
+
expect(trigger(container)).toHaveAttribute("aria-expanded", "true");
|
|
55
|
+
|
|
56
|
+
fireEvent.click(screen.getByRole("option", { name: /English/ }));
|
|
57
|
+
expect(trigger(container)).toHaveAttribute("aria-expanded", "false");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders nothing when there are no languages", () => {
|
|
61
|
+
const { container } = render(<LanguageSwitcher languages={[]} />);
|
|
62
|
+
expect(container).toBeEmptyDOMElement();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { LogoCloud } from "./LogoCloud";
|
|
4
|
+
|
|
5
|
+
const logos = [{ name: "Acme" }, { name: "Globex" }];
|
|
6
|
+
|
|
7
|
+
describe("LogoCloud", () => {
|
|
8
|
+
it("renders the title and logo names (quiet by default)", () => {
|
|
9
|
+
render(<LogoCloud title="Teams" logos={logos} />);
|
|
10
|
+
expect(screen.getByText("Teams")).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByText("Acme")).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByText("Globex")).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("does not render a title when none is provided", () => {
|
|
16
|
+
render(<LogoCloud logos={logos} />);
|
|
17
|
+
expect(screen.queryByText("Teams")).not.toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders the marquee option (items duplicated for the loop)", () => {
|
|
21
|
+
render(<LogoCloud logos={logos} option="marquee" />);
|
|
22
|
+
expect(screen.getAllByText("Acme").length).toBeGreaterThan(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders the strip option", () => {
|
|
26
|
+
render(<LogoCloud logos={logos} option="strip" />);
|
|
27
|
+
expect(screen.getByText("Acme")).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders a logo image when src is provided", () => {
|
|
31
|
+
render(<LogoCloud logos={[{ name: "Acme", src: "/acme.png" }]} />);
|
|
32
|
+
const img = screen.getByAltText("Acme");
|
|
33
|
+
expect(img).toHaveAttribute("src", "/acme.png");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { MegaMenu } from "./MegaMenu";
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{
|
|
7
|
+
label: "Product",
|
|
8
|
+
columns: [
|
|
9
|
+
{
|
|
10
|
+
title: "Build",
|
|
11
|
+
links: [
|
|
12
|
+
{ label: "API Platform", description: "REST + GraphQL", href: "#api" },
|
|
13
|
+
{ label: "Workflows", href: "#wf" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
featured: { title: "Get started", description: "See it live", cta: "Start", href: "#start" },
|
|
18
|
+
},
|
|
19
|
+
{ label: "Pricing", href: "#pricing" },
|
|
20
|
+
{ label: "Docs", href: "#docs" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function desktopTrigger(container: HTMLElement): HTMLButtonElement {
|
|
24
|
+
// The desktop trigger is the one with aria-haspopup; the mobile accordion
|
|
25
|
+
// button does not set it, and it is hidden behind `lg:hidden` CSS.
|
|
26
|
+
return container.querySelector('button[aria-haspopup="true"]')!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("MegaMenu", () => {
|
|
30
|
+
it("renders the brand and plain link items", () => {
|
|
31
|
+
render(<MegaMenu brand="Acurio" items={items} />);
|
|
32
|
+
expect(screen.getByText("Acurio")).toBeInTheDocument();
|
|
33
|
+
// Links render in both the desktop bar and the mobile panel.
|
|
34
|
+
const pricingLinks = screen.getAllByRole("link", { name: "Pricing" });
|
|
35
|
+
expect(pricingLinks.length).toBeGreaterThan(0);
|
|
36
|
+
expect(pricingLinks[0]).toHaveAttribute("href", "#pricing");
|
|
37
|
+
expect(screen.getAllByRole("link", { name: "Docs" }).length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders a trigger button for panel items (with columns/featured)", () => {
|
|
41
|
+
const { container } = render(<MegaMenu brand="Acurio" items={items} />);
|
|
42
|
+
const trigger = desktopTrigger(container);
|
|
43
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "true");
|
|
44
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reveals the panel (column links + featured card) when the desktop trigger is clicked", () => {
|
|
48
|
+
const { container } = render(<MegaMenu brand="Acurio" items={items} />);
|
|
49
|
+
fireEvent.click(desktopTrigger(container));
|
|
50
|
+
|
|
51
|
+
expect(container.querySelector('[role="region"][aria-label="Product menu"]')).toBeInTheDocument();
|
|
52
|
+
// The column and featured links are present in the panel markup.
|
|
53
|
+
expect(container.querySelector('a[href="#api"]')).toBeInTheDocument();
|
|
54
|
+
expect(container.querySelector('a[href="#wf"]')).toBeInTheDocument();
|
|
55
|
+
expect(container.querySelector('a[href="#start"]')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("sets aria-expanded while the panel is open and closes on Escape", () => {
|
|
59
|
+
const { container } = render(<MegaMenu brand="Acurio" items={items} />);
|
|
60
|
+
const trigger = desktopTrigger(container);
|
|
61
|
+
fireEvent.click(trigger);
|
|
62
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
63
|
+
|
|
64
|
+
fireEvent.keyDown(screen.getByRole("banner"), { key: "Escape" });
|
|
65
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("applies the inverse variant to the header shell", () => {
|
|
69
|
+
render(<MegaMenu brand="Acurio" items={items} variant="inverse" />);
|
|
70
|
+
expect(screen.getByRole("banner")).toHaveClass("bg-[#101010]/85");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("toggles the mobile menu via the hamburger", () => {
|
|
74
|
+
render(<MegaMenu brand="Acurio" items={items} />);
|
|
75
|
+
const openBtn = screen.getByRole("button", { name: "Open menu" });
|
|
76
|
+
expect(openBtn).toHaveAttribute("aria-expanded", "false");
|
|
77
|
+
|
|
78
|
+
fireEvent.click(openBtn);
|
|
79
|
+
expect(screen.getByRole("button", { name: "Close menu" })).toHaveAttribute(
|
|
80
|
+
"aria-expanded",
|
|
81
|
+
"true",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not render sticky when sticky is disabled", () => {
|
|
86
|
+
render(<MegaMenu brand="Acurio" items={items} sticky={false} />);
|
|
87
|
+
expect(screen.getByRole("banner")).not.toHaveClass("sticky");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Modal } from "./Modal";
|
|
5
|
+
|
|
6
|
+
// The Modal drives the HTML Popover API, which jsdom doesn't implement, and a
|
|
7
|
+
// `[popover]` element is treated as hidden by Testing Library's role queries.
|
|
8
|
+
// Stub the popover surface and query the rendered structure directly.
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
if (!HTMLElement.prototype.showPopover) {
|
|
11
|
+
HTMLElement.prototype.showPopover = vi.fn();
|
|
12
|
+
}
|
|
13
|
+
if (!HTMLElement.prototype.hidePopover) {
|
|
14
|
+
HTMLElement.prototype.hidePopover = vi.fn();
|
|
15
|
+
}
|
|
16
|
+
const originalMatches = Element.prototype.matches;
|
|
17
|
+
Element.prototype.matches = function (selector: string) {
|
|
18
|
+
if (selector === ":popover-open") return false;
|
|
19
|
+
return originalMatches.call(this, selector);
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Modal", () => {
|
|
24
|
+
it("renders the dialog with title and description when open", () => {
|
|
25
|
+
const onClose = vi.fn();
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<Modal open onClose={onClose} title="Title" description="Description">
|
|
28
|
+
Body
|
|
29
|
+
</Modal>,
|
|
30
|
+
);
|
|
31
|
+
expect(container.querySelector('[role="dialog"]')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText("Description")).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText("Body")).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("is marked as a modal dialog and links title via aria-labelledby", () => {
|
|
38
|
+
const onClose = vi.fn();
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<Modal open onClose={onClose} title="Confirm">
|
|
41
|
+
Body
|
|
42
|
+
</Modal>,
|
|
43
|
+
);
|
|
44
|
+
const dialog = container.querySelector('[role="dialog"]');
|
|
45
|
+
expect(dialog).toHaveAttribute("aria-modal", "true");
|
|
46
|
+
const labelledBy = dialog!.getAttribute("aria-labelledby")!;
|
|
47
|
+
expect(document.getElementById(labelledBy)).toHaveTextContent("Confirm");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps its content in the DOM even when closed (hidden via popover)", () => {
|
|
51
|
+
const onClose = vi.fn();
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<Modal open={false} onClose={onClose} title="Title">
|
|
54
|
+
Body
|
|
55
|
+
</Modal>,
|
|
56
|
+
);
|
|
57
|
+
// The component always renders the dialog markup; open/close is handled by
|
|
58
|
+
// the native popover, so the markup is present but hidden.
|
|
59
|
+
expect(container.querySelector('[role="dialog"]')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("calls onClose when the close button is pressed", async () => {
|
|
63
|
+
const onClose = vi.fn();
|
|
64
|
+
const { container } = render(<Modal open onClose={onClose} title="Title" />);
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
const closeBtn = container.querySelector('button[aria-label="Close dialog"]')!;
|
|
67
|
+
await user.click(closeBtn);
|
|
68
|
+
expect(onClose).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders a footer", () => {
|
|
72
|
+
const onClose = vi.fn();
|
|
73
|
+
render(
|
|
74
|
+
<Modal open onClose={onClose} footer={<button>Go</button>}>
|
|
75
|
+
Body
|
|
76
|
+
</Modal>,
|
|
77
|
+
);
|
|
78
|
+
expect(screen.getByText("Go")).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Navbar } from "./Navbar";
|
|
5
|
+
|
|
6
|
+
const links = [
|
|
7
|
+
{ label: "Product", href: "#product" },
|
|
8
|
+
{ label: "Pricing", href: "#pricing" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("Navbar", () => {
|
|
12
|
+
it("renders brand, nav links and the action slot", () => {
|
|
13
|
+
render(<Navbar brand="Acurio" links={links} cta={<button>Start</button>} />);
|
|
14
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText("Acurio")).toBeInTheDocument();
|
|
17
|
+
// Both the desktop list and the mobile panel render a "Product" link.
|
|
18
|
+
const productLinks = within(nav).getAllByRole("link", { name: "Product" });
|
|
19
|
+
expect(productLinks.length).toBeGreaterThan(0);
|
|
20
|
+
expect(productLinks[0]).toHaveAttribute("href", "#product");
|
|
21
|
+
expect(screen.getAllByRole("button", { name: "Start" }).length).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders the brand href via brandHref", () => {
|
|
25
|
+
render(<Navbar brand="Acurio" links={links} brandHref="/" />);
|
|
26
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
27
|
+
expect(nav.querySelector('a[href="/"]')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("applies the classic variant shell", () => {
|
|
31
|
+
render(<Navbar brand="Acurio" links={links} variant="classic" />);
|
|
32
|
+
expect(screen.getByRole("banner")).toHaveClass("bg-background/85");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies the inverse variant shell and on-secondary link tokens", () => {
|
|
36
|
+
render(<Navbar brand="Acurio" links={links} variant="inverse" />);
|
|
37
|
+
expect(screen.getByRole("banner")).toHaveClass("bg-[#101010]/85");
|
|
38
|
+
const productLinks = screen.getAllByRole("link", { name: "Product" });
|
|
39
|
+
expect(productLinks[0]).toHaveClass("text-on-secondary/60");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders a custom logo instead of the wordmark", () => {
|
|
43
|
+
render(
|
|
44
|
+
<Navbar brand="Acurio" links={links} logo={<span data-testid="logo">Logo</span>} />,
|
|
45
|
+
);
|
|
46
|
+
expect(screen.getByTestId("logo")).toBeInTheDocument();
|
|
47
|
+
expect(screen.queryByText("Acurio")).not.toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("starts collapsed and toggles the mobile menu via the hamburger", async () => {
|
|
51
|
+
const user = userEvent.setup();
|
|
52
|
+
render(<Navbar brand="Acurio" links={links} />);
|
|
53
|
+
expect(screen.getByRole("button", { name: "Open menu" })).toHaveAttribute(
|
|
54
|
+
"aria-expanded",
|
|
55
|
+
"false",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await user.click(screen.getByRole("button", { name: "Open menu" }));
|
|
59
|
+
expect(screen.getByRole("button", { name: "Close menu" })).toHaveAttribute(
|
|
60
|
+
"aria-expanded",
|
|
61
|
+
"true",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await user.click(screen.getByRole("button", { name: "Close menu" }));
|
|
65
|
+
expect(screen.getByRole("button", { name: "Open menu" })).toHaveAttribute(
|
|
66
|
+
"aria-expanded",
|
|
67
|
+
"false",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("closes the mobile menu when a mobile link is clicked", async () => {
|
|
72
|
+
const user = userEvent.setup();
|
|
73
|
+
render(<Navbar brand="Acurio" links={links} />);
|
|
74
|
+
|
|
75
|
+
await user.click(screen.getByRole("button", { name: "Open menu" }));
|
|
76
|
+
expect(screen.getByRole("button", { name: "Close menu" })).toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
// Two "Product" links exist (desktop + mobile); tapping one collapses.
|
|
79
|
+
const product = screen.getAllByRole("link", { name: "Product" })[1];
|
|
80
|
+
await user.click(product);
|
|
81
|
+
expect(screen.getByRole("button", { name: "Open menu" })).toHaveAttribute(
|
|
82
|
+
"aria-expanded",
|
|
83
|
+
"false",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("is not sticky when sticky is disabled", () => {
|
|
88
|
+
render(<Navbar brand="Acurio" links={links} sticky={false} />);
|
|
89
|
+
expect(screen.getByRole("banner")).not.toHaveClass("sticky");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Newsletter } from "./Newsletter";
|
|
5
|
+
|
|
6
|
+
describe("Newsletter", () => {
|
|
7
|
+
it("renders the field and submit button (inline by default)", () => {
|
|
8
|
+
render(<Newsletter />);
|
|
9
|
+
expect(screen.getByLabelText("Your email address")).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole("button", { name: "Join" })).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("calls onSubmit with a valid email and shows the success message", async () => {
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
const onSubmit = vi.fn();
|
|
16
|
+
render(<Newsletter onSubmit={onSubmit} successMessage="You're in!" />);
|
|
17
|
+
await user.type(screen.getByLabelText("Your email address"), "foo@bar.com");
|
|
18
|
+
await user.click(screen.getByRole("button", { name: "Join" }));
|
|
19
|
+
|
|
20
|
+
expect(onSubmit).toHaveBeenCalledWith("foo@bar.com");
|
|
21
|
+
expect(screen.getByRole("status")).toHaveTextContent("You're in!");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("shows a validation error when an empty/invalid value is submitted", async () => {
|
|
25
|
+
const user = userEvent.setup();
|
|
26
|
+
const onSubmit = vi.fn();
|
|
27
|
+
render(<Newsletter onSubmit={onSubmit} />);
|
|
28
|
+
// Submit without entering a value; the component's own regex rejects it.
|
|
29
|
+
await user.click(screen.getByRole("button", { name: "Join" }));
|
|
30
|
+
|
|
31
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
32
|
+
expect(screen.getByRole("alert")).toHaveTextContent("valid email");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders the note text when provided", () => {
|
|
36
|
+
render(<Newsletter note="Once a month." />);
|
|
37
|
+
expect(screen.getByText("Once a month.")).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders the card and underline options", () => {
|
|
41
|
+
const { container } = render(<Newsletter option="card" />);
|
|
42
|
+
expect(container.querySelector("form")).toBeInTheDocument();
|
|
43
|
+
render(<Newsletter option="underline" />);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { Pricing } from "./Pricing";
|
|
4
|
+
|
|
5
|
+
const plans = [
|
|
6
|
+
{
|
|
7
|
+
name: "Starter",
|
|
8
|
+
monthly: 9,
|
|
9
|
+
yearly: 90,
|
|
10
|
+
features: ["1 project"],
|
|
11
|
+
cta: "Get started",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "Pro",
|
|
15
|
+
monthly: 29,
|
|
16
|
+
yearly: 290,
|
|
17
|
+
highlighted: true,
|
|
18
|
+
features: ["Unlimited projects"],
|
|
19
|
+
cta: "Go Pro",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
describe("Pricing", () => {
|
|
24
|
+
it("renders plan names, prices and the cards option by default", () => {
|
|
25
|
+
render(<Pricing plans={plans} />);
|
|
26
|
+
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByText("Pro")).toBeInTheDocument();
|
|
28
|
+
// Monthly price for the starter plan is $9 (default billing = monthly).
|
|
29
|
+
expect(screen.getByText("$9")).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("switches between monthly and yearly billing via the toggle", () => {
|
|
33
|
+
render(<Pricing plans={[plans[0]]} />);
|
|
34
|
+
expect(screen.getByText("$9")).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
fireEvent.click(screen.getByRole("switch"));
|
|
37
|
+
expect(screen.getByText("$90")).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders the compact option", () => {
|
|
41
|
+
render(<Pricing plans={plans} option="compact" />);
|
|
42
|
+
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders the bento option", () => {
|
|
46
|
+
render(<Pricing plans={plans} option="bento" />);
|
|
47
|
+
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("calls onSelect with the plan and current billing when a plan button is pressed", () => {
|
|
51
|
+
const onSelect = vi.fn();
|
|
52
|
+
render(<Pricing plans={[plans[0]]} onSelect={onSelect} />);
|
|
53
|
+
fireEvent.click(screen.getByRole("button", { name: "Get started" }));
|
|
54
|
+
expect(onSelect).toHaveBeenCalledWith(plans[0], "monthly");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders a price label for custom (null) pricing", () => {
|
|
58
|
+
render(
|
|
59
|
+
<Pricing
|
|
60
|
+
plans={[
|
|
61
|
+
{ name: "Enterprise", monthly: null, yearly: null, features: [], cta: "Contact" },
|
|
62
|
+
]}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByText("Custom")).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Prose, ProseLead } from "./Prose";
|
|
4
|
+
|
|
5
|
+
describe("Prose", () => {
|
|
6
|
+
it("renders an article element with prose classes", () => {
|
|
7
|
+
render(<Prose>Text</Prose>);
|
|
8
|
+
const article = screen.getByText("Text");
|
|
9
|
+
expect(article.tagName).toBe("ARTICLE");
|
|
10
|
+
expect(article).toHaveClass("prose");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("applies the requested size", () => {
|
|
14
|
+
render(<Prose size="xl">Text</Prose>);
|
|
15
|
+
expect(screen.getByText("Text")).toHaveClass("prose-xl");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("caps width by default and removes it when wide", () => {
|
|
19
|
+
render(<Prose>Capped</Prose>);
|
|
20
|
+
expect(screen.getByText("Capped")).toHaveClass("max-w-3xl");
|
|
21
|
+
|
|
22
|
+
render(<Prose wide>Wide</Prose>);
|
|
23
|
+
expect(screen.getByText("Wide")).not.toHaveClass("max-w-3xl");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders children", () => {
|
|
27
|
+
render(
|
|
28
|
+
<Prose>
|
|
29
|
+
<h1>Heading</h1>
|
|
30
|
+
</Prose>,
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders raw HTML via the html prop instead of children", () => {
|
|
36
|
+
render(<Prose html="<h2>CMS content</h2><p>A paragraph.</p>" />);
|
|
37
|
+
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("CMS content");
|
|
38
|
+
expect(screen.getByText("A paragraph.")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("ProseLead", () => {
|
|
43
|
+
it("renders a lead-styled paragraph", () => {
|
|
44
|
+
render(<ProseLead>Intro</ProseLead>);
|
|
45
|
+
const p = screen.getByText("Intro");
|
|
46
|
+
expect(p.tagName).toBe("P");
|
|
47
|
+
expect(p).toHaveClass("lead");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Section, SectionHeader } from "./Section";
|
|
4
|
+
|
|
5
|
+
describe("Section", () => {
|
|
6
|
+
it("renders a section element with padding for the size", () => {
|
|
7
|
+
render(<Section size="lg">Body</Section>);
|
|
8
|
+
const section = screen.getByText("Body").closest("section");
|
|
9
|
+
expect(section).toBeInTheDocument();
|
|
10
|
+
expect(section).toHaveClass("py-24");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders children", () => {
|
|
14
|
+
render(<Section>Body</Section>);
|
|
15
|
+
expect(screen.getByText("Body")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("forwards an id", () => {
|
|
19
|
+
render(<Section id="pricing">Pricing</Section>);
|
|
20
|
+
expect(screen.getByText("Pricing").closest("section")).toHaveAttribute("id", "pricing");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders a SectionHeader with eyebrow/title/description when provided", () => {
|
|
24
|
+
render(
|
|
25
|
+
<Section eyebrow="E" title="T" description="D">
|
|
26
|
+
Body
|
|
27
|
+
</Section>,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText("E")).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText("T")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText("D")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("SectionHeader", () => {
|
|
36
|
+
it("renders eyebrow, title and description", () => {
|
|
37
|
+
render(<SectionHeader eyebrow="E" title="T" description="D" />);
|
|
38
|
+
expect(screen.getByText("E")).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText("T")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("D")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("centers by default and aligns left when requested", () => {
|
|
44
|
+
const { rerender } = render(<SectionHeader title="T" />);
|
|
45
|
+
expect(screen.getByText("T").parentElement).toHaveClass("text-center");
|
|
46
|
+
|
|
47
|
+
rerender(<SectionHeader title="T" align="left" />);
|
|
48
|
+
// left is the base; center adds mx-auto text-center.
|
|
49
|
+
expect(screen.getByText("T").parentElement).not.toHaveClass("text-center");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Slot } from "./Slot";
|
|
4
|
+
|
|
5
|
+
describe("Slot", () => {
|
|
6
|
+
it("renders the single child with merged props", () => {
|
|
7
|
+
render(
|
|
8
|
+
<Slot className="btn" data-testid="slot">
|
|
9
|
+
<span>Hello</span>
|
|
10
|
+
</Slot>,
|
|
11
|
+
);
|
|
12
|
+
const el = screen.getByTestId("slot");
|
|
13
|
+
expect(el.tagName).toBe("SPAN");
|
|
14
|
+
expect(el).toHaveClass("btn");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("forwards an onClick to the child and calls both handlers", () => {
|
|
18
|
+
const slotClick = vi.fn();
|
|
19
|
+
const childClick = vi.fn();
|
|
20
|
+
render(
|
|
21
|
+
<Slot onClick={slotClick}>
|
|
22
|
+
<button onClick={childClick}>Go</button>
|
|
23
|
+
</Slot>,
|
|
24
|
+
);
|
|
25
|
+
const btn = screen.getByRole("button", { name: "Go" });
|
|
26
|
+
btn.click();
|
|
27
|
+
expect(childClick).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders nothing when there is no single valid child", () => {
|
|
32
|
+
const { container } = render(<Slot>text only</Slot>);
|
|
33
|
+
// A plain string is not a valid React element, so Slot returns null.
|
|
34
|
+
expect(container).toBeEmptyDOMElement();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("merges className with the child", () => {
|
|
38
|
+
render(
|
|
39
|
+
<Slot className="slot-class">
|
|
40
|
+
<div className="child-class">x</div>
|
|
41
|
+
</Slot>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByText("x")).toHaveClass("slot-class child-class");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Stats } from "./Stats";
|
|
4
|
+
|
|
5
|
+
const stats = [
|
|
6
|
+
{ value: "12.4K", label: "Active users", delta: 18.2 },
|
|
7
|
+
{ value: "98", suffix: "%", label: "Satisfaction", accent: true },
|
|
8
|
+
{ value: "1.8", suffix: "ms", label: "Response" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("Stats", () => {
|
|
12
|
+
it("renders labels and values with the editorial option by default", () => {
|
|
13
|
+
render(<Stats stats={stats} />);
|
|
14
|
+
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByText("12.4K")).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText("Satisfaction")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders a suffix inline with the value", () => {
|
|
20
|
+
render(<Stats stats={[stats[1]]} />);
|
|
21
|
+
expect(screen.getByText("%")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders delta badges for statistics that have a delta", () => {
|
|
25
|
+
render(<Stats stats={[stats[0]]} />);
|
|
26
|
+
expect(screen.getByText("18.2%")).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders all option layouts", () => {
|
|
30
|
+
const { rerender } = render(<Stats stats={stats} option="editorial" />);
|
|
31
|
+
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
rerender(<Stats stats={stats} option="hairline" />);
|
|
34
|
+
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
rerender(<Stats stats={stats} option="cells" />);
|
|
37
|
+
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
38
|
+
|
|
39
|
+
rerender(<Stats stats={stats} option="ticker" />);
|
|
40
|
+
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("uses a downward delta badge for negative changes", () => {
|
|
44
|
+
render(<Stats stats={[{ value: "1.8", label: "Response", delta: -24.6 }]} />);
|
|
45
|
+
expect(screen.getByText("24.6%")).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|