mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
chore: add unit tests
This commit is contained in:
@@ -60,9 +60,11 @@ Every time a new Front-end dependency is added by you or others, you will need t
|
||||
- `pnpm start` - Start production server
|
||||
- `pnpm lint` - Run ESLint and Prettier checks
|
||||
- `pnpm format` - Format code with Prettier
|
||||
- `pnpm type-check` - Run TypeScript type checking
|
||||
- `pnpm types` - Run TypeScript type checking
|
||||
- `pnpm test` - Run Playwright tests
|
||||
- `pnpm test-ui` - Run Playwright tests with UI
|
||||
- `pnpm test:ui` - Run Playwright tests with UI
|
||||
- `pnpm test:unit` - Run unit tests (Vitest)
|
||||
- `pnpm test:unit:watch` - Run unit tests (Vitest) in watch mode
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
@@ -96,14 +98,14 @@ Storybook is a powerful development environment for UI components. It allows you
|
||||
To build a static version of Storybook for deployment, use:
|
||||
|
||||
```bash
|
||||
pnpm build-storybook
|
||||
pnpm build:storybook
|
||||
```
|
||||
|
||||
3. **Running Storybook Tests**:
|
||||
Storybook tests can be run using:
|
||||
|
||||
```bash
|
||||
pnpm test-storybook
|
||||
pnpm test:storybook
|
||||
```
|
||||
|
||||
4. **Writing Stories**:
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
"start:standalone": "cd .next/standalone && node server.js",
|
||||
"lint": "next lint && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"type-check": "tsc --noEmit",
|
||||
"types": "tsc --noEmit",
|
||||
"test": "next build --turbo && playwright test",
|
||||
"test-ui": "next build --turbo && playwright test --ui",
|
||||
"test:no-build": "playwright test",
|
||||
"test:ui": "next build --turbo && playwright test --ui",
|
||||
"test:unit": "vitest --config vitest.config.mjs --run",
|
||||
"test:unit:watch": "vitest --config vitest.config.mjs --watch",
|
||||
"gentests": "playwright codegen http://localhost:3000",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"test-storybook": "test-storybook",
|
||||
"build:storybook": "storybook build",
|
||||
"fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/api/openapi.json && prettier --write ./src/api/openapi.json",
|
||||
"generate:api-client": "orval --config ./orval.config.ts",
|
||||
"generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client"
|
||||
@@ -102,6 +102,9 @@
|
||||
"@storybook/nextjs": "9.0.12",
|
||||
"@tanstack/eslint-plugin-query": "5.78.0",
|
||||
"@tanstack/react-query-devtools": "5.80.10",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/lodash": "4.17.18",
|
||||
"@types/negotiator": "0.6.4",
|
||||
@@ -109,6 +112,7 @@
|
||||
"@types/react": "18.3.17",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"axe-playwright": "2.1.0",
|
||||
"chromatic": "11.25.2",
|
||||
"concurrently": "9.1.2",
|
||||
@@ -116,6 +120,7 @@
|
||||
"eslint-config-next": "15.3.4",
|
||||
"eslint-plugin-storybook": "9.0.12",
|
||||
"import-in-the-middle": "1.14.2",
|
||||
"jsdom": "26.1.0",
|
||||
"msw": "2.10.2",
|
||||
"msw-storybook-addon": "2.0.5",
|
||||
"orval": "7.10.0",
|
||||
@@ -125,7 +130,9 @@
|
||||
"require-in-the-middle": "7.5.2",
|
||||
"storybook": "9.0.12",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.0",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
913
autogpt_platform/frontend/pnpm-lock.yaml
generated
913
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Button } from "./Button";
|
||||
|
||||
describe("Button Component", () => {
|
||||
it("renders button with text", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Click me" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClick when clicked", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Button onClick={handleClick}>Click me</Button>);
|
||||
|
||||
await user.click(screen.getByRole("button"));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("is disabled when disabled prop is true", () => {
|
||||
render(<Button disabled>Disabled button</Button>);
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("does not call onClick when disabled", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Button disabled onClick={handleClick}>
|
||||
Disabled button
|
||||
</Button>,
|
||||
);
|
||||
|
||||
// Try to click the disabled button
|
||||
await user.click(screen.getByRole("button"));
|
||||
expect(handleClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders with different variants", () => {
|
||||
const { rerender } = render(<Button variant="primary">Primary</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("bg-zinc-800");
|
||||
|
||||
rerender(<Button variant="secondary">Secondary</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("bg-zinc-100");
|
||||
|
||||
rerender(<Button variant="destructive">Destructive</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("bg-red-500");
|
||||
|
||||
rerender(<Button variant="outline">Outline</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("border-zinc-700");
|
||||
|
||||
rerender(<Button variant="ghost">Ghost</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("bg-transparent");
|
||||
});
|
||||
|
||||
// Sizes and variants styling are covered by Chromatic via Storybook
|
||||
|
||||
it("shows loading state", () => {
|
||||
render(<Button loading>Loading button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("pointer-events-none");
|
||||
|
||||
// Check for loading spinner (svg element)
|
||||
const spinner = button.querySelector("svg");
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveClass("animate-spin");
|
||||
});
|
||||
|
||||
it("renders with left icon", () => {
|
||||
const TestIcon = () => <span data-testid="test-icon">Icon</span>;
|
||||
|
||||
render(<Button leftIcon={<TestIcon />}>Button with left icon</Button>);
|
||||
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("Button with left icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with right icon", () => {
|
||||
const TestIcon = () => <span data-testid="test-icon">Icon</span>;
|
||||
|
||||
render(<Button rightIcon={<TestIcon />}>Button with right icon</Button>);
|
||||
|
||||
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("Button with right icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports keyboard navigation", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Button onClick={handleClick}>Keyboard button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
|
||||
// Focus the button
|
||||
await user.tab();
|
||||
expect(button).toHaveFocus();
|
||||
|
||||
// Press Enter
|
||||
await user.keyboard("{Enter}");
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Press Space
|
||||
await user.keyboard(" ");
|
||||
expect(handleClick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<Button className="custom-class">Custom button</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("handles double click", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Button onClick={handleClick}>Double click me</Button>);
|
||||
|
||||
await user.dblClick(screen.getByRole("button"));
|
||||
expect(handleClick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("maintains focus after click", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Button>Focus test</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
await user.click(button);
|
||||
expect(button).toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -1,275 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { Play, Plus } from "lucide-react";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { Button } from "./Button";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Atoms/Tests/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["!autodocs"],
|
||||
args: {
|
||||
onClick: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Test button click functionality
|
||||
export const ClickInteraction: Story = {
|
||||
args: {
|
||||
children: "Click Me",
|
||||
variant: "primary",
|
||||
},
|
||||
play: async function testButtonClick({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Click Me" });
|
||||
|
||||
// Test initial state
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).not.toBeDisabled();
|
||||
|
||||
// Test click interaction
|
||||
await userEvent.click(button);
|
||||
|
||||
// Assert the click handler was called
|
||||
expect(args.onClick).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
// Test disabled button behavior
|
||||
export const DisabledInteraction: Story = {
|
||||
args: {
|
||||
children: "Disabled Button",
|
||||
variant: "primary",
|
||||
disabled: true,
|
||||
},
|
||||
play: async function testDisabledButton({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Disabled Button" });
|
||||
|
||||
// Test disabled state
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveAttribute("disabled");
|
||||
|
||||
// Test that disabled button has proper styling (pointer-events: none prevents clicking)
|
||||
// We don't test clicking because disabled buttons with pointer-events: none can't be clicked
|
||||
},
|
||||
};
|
||||
|
||||
// Test loading button behavior
|
||||
export const LoadingInteraction: Story = {
|
||||
args: {
|
||||
children: "Loading Button",
|
||||
variant: "primary",
|
||||
loading: true,
|
||||
},
|
||||
play: async function testLoadingButton({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Loading Button" });
|
||||
|
||||
// Test loading state - button should show loading spinner
|
||||
const spinner = button.querySelector("svg");
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveClass("animate-spin");
|
||||
|
||||
// Test that loading button is still clickable but pointer events are disabled
|
||||
expect(button).toHaveClass("pointer-events-none");
|
||||
},
|
||||
};
|
||||
|
||||
// Test keyboard navigation
|
||||
export const KeyboardInteraction: Story = {
|
||||
args: {
|
||||
children: "Keyboard Test",
|
||||
variant: "primary",
|
||||
},
|
||||
play: async function testKeyboardNavigation({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Keyboard Test" });
|
||||
|
||||
// Test tab navigation
|
||||
await userEvent.tab();
|
||||
expect(button).toHaveFocus();
|
||||
|
||||
// Test Enter key activation
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(args.onClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key activation
|
||||
await userEvent.keyboard(" ");
|
||||
expect(args.onClick).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
};
|
||||
|
||||
// Test focus and blur events
|
||||
export const FocusInteraction: Story = {
|
||||
args: {
|
||||
children: "Focus Test",
|
||||
variant: "outline",
|
||||
},
|
||||
play: async function testFocusEvents({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Focus Test" });
|
||||
|
||||
// Test programmatic focus
|
||||
button.focus();
|
||||
expect(button).toHaveFocus();
|
||||
|
||||
// Test blur
|
||||
button.blur();
|
||||
expect(button).not.toHaveFocus();
|
||||
|
||||
// Test click focus
|
||||
await userEvent.click(button);
|
||||
expect(button).toHaveFocus();
|
||||
},
|
||||
};
|
||||
|
||||
// Test different variants work correctly
|
||||
export const VariantsInteraction: Story = {
|
||||
render: function renderVariants(args) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Button variant="primary" onClick={args.onClick}>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={args.onClick}>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={args.onClick}>
|
||||
Destructive
|
||||
</Button>
|
||||
<Button variant="outline" onClick={args.onClick}>
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={args.onClick}>
|
||||
Ghost
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async function testVariants({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test all variants are rendered
|
||||
const primaryBtn = canvas.getByRole("button", { name: "Primary" });
|
||||
const secondaryBtn = canvas.getByRole("button", { name: "Secondary" });
|
||||
const destructiveBtn = canvas.getByRole("button", { name: "Destructive" });
|
||||
const outlineBtn = canvas.getByRole("button", { name: "Outline" });
|
||||
const ghostBtn = canvas.getByRole("button", { name: "Ghost" });
|
||||
|
||||
expect(primaryBtn).toBeInTheDocument();
|
||||
expect(secondaryBtn).toBeInTheDocument();
|
||||
expect(destructiveBtn).toBeInTheDocument();
|
||||
expect(outlineBtn).toBeInTheDocument();
|
||||
expect(ghostBtn).toBeInTheDocument();
|
||||
|
||||
// Test clicking each variant
|
||||
await userEvent.click(primaryBtn);
|
||||
await userEvent.click(secondaryBtn);
|
||||
await userEvent.click(destructiveBtn);
|
||||
await userEvent.click(outlineBtn);
|
||||
await userEvent.click(ghostBtn);
|
||||
|
||||
// Assert all clicks were registered
|
||||
expect(args.onClick).toHaveBeenCalledTimes(5);
|
||||
},
|
||||
};
|
||||
|
||||
// Test button with icons
|
||||
export const IconInteraction: Story = {
|
||||
args: {
|
||||
children: "With Icon",
|
||||
variant: "primary",
|
||||
leftIcon: <Play className="h-4 w-4" />,
|
||||
rightIcon: <Plus className="h-4 w-4" />,
|
||||
},
|
||||
play: async function testIconButton({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "With Icon" });
|
||||
|
||||
// Test button with icons is rendered correctly
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
// Test icons are present
|
||||
const icons = button.querySelectorAll("svg");
|
||||
expect(icons).toHaveLength(2); // leftIcon + rightIcon
|
||||
|
||||
// Test button functionality with icons
|
||||
await userEvent.click(button);
|
||||
expect(args.onClick).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
// Test icon-only button
|
||||
export const IconOnlyInteraction: Story = {
|
||||
args: {
|
||||
children: <Plus className="h-4 w-4" />,
|
||||
variant: "icon",
|
||||
size: "icon",
|
||||
"aria-label": "Add item",
|
||||
},
|
||||
play: async function testIconOnlyButton({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Add item" });
|
||||
|
||||
// Test icon-only button accessibility
|
||||
expect(button).toHaveAccessibleName("Add item");
|
||||
|
||||
// Test icon is present
|
||||
const icon = button.querySelector("svg");
|
||||
expect(icon).toBeInTheDocument();
|
||||
|
||||
// Test functionality
|
||||
await userEvent.click(button);
|
||||
expect(args.onClick).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
// Test multiple clicks and double click
|
||||
export const MultipleClicksInteraction: Story = {
|
||||
args: {
|
||||
children: "Multi Click",
|
||||
variant: "secondary",
|
||||
},
|
||||
play: async function testMultipleClicks({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Multi Click" });
|
||||
|
||||
// Test multiple single clicks
|
||||
await userEvent.click(button);
|
||||
await userEvent.click(button);
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(args.onClick).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Test double click
|
||||
await userEvent.dblClick(button);
|
||||
expect(args.onClick).toHaveBeenCalledTimes(5); // 3 + 2 from double click
|
||||
},
|
||||
};
|
||||
|
||||
// Test hover states
|
||||
export const HoverInteraction: Story = {
|
||||
args: {
|
||||
children: "Hover Me",
|
||||
variant: "outline",
|
||||
},
|
||||
play: async function testHoverStates({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: "Hover Me" });
|
||||
|
||||
// Test hover interaction
|
||||
await userEvent.hover(button);
|
||||
|
||||
// Test unhover
|
||||
await userEvent.unhover(button);
|
||||
|
||||
// Verify button is still functional after hover interactions
|
||||
expect(button).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Input } from "./Input";
|
||||
|
||||
describe("Input Component", () => {
|
||||
it("renders input with label", () => {
|
||||
render(<Input label="Username" />);
|
||||
|
||||
expect(screen.getByLabelText("Username")).toBeInTheDocument();
|
||||
expect(screen.getByText("Username")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders input with hidden label", () => {
|
||||
render(<Input label="Username" hideLabel />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("aria-label", "Username");
|
||||
expect(screen.queryByText("Username")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onChange when typing", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Input label="Username" onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
await user.type(input, "test");
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
expect(input).toHaveValue("test");
|
||||
});
|
||||
|
||||
it("displays placeholder text", () => {
|
||||
render(<Input label="Username" placeholder="Enter your username" />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
expect(input).toHaveAttribute("placeholder", "Enter your username");
|
||||
});
|
||||
|
||||
it("uses label as placeholder when no placeholder provided", () => {
|
||||
render(<Input label="Username" />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
expect(input).toHaveAttribute("placeholder", "Username");
|
||||
});
|
||||
|
||||
it("displays error message", () => {
|
||||
render(<Input label="Username" error="Username is required" />);
|
||||
|
||||
expect(screen.getByText("Username is required")).toBeInTheDocument();
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("is disabled when disabled prop is true", () => {
|
||||
render(<Input label="Username" disabled />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it("does not call onChange when disabled", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Input label="Username" disabled onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
await user.type(input, "test");
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
|
||||
it("supports different input types", () => {
|
||||
const { rerender } = render(<Input label="Email" type="email" />);
|
||||
expect(screen.getByLabelText("Email")).toHaveAttribute("type", "email");
|
||||
|
||||
rerender(<Input label="Password" type="password" />);
|
||||
expect(screen.getByLabelText("Password")).toHaveAttribute(
|
||||
"type",
|
||||
"password",
|
||||
);
|
||||
|
||||
rerender(<Input label="Number" type="number" />);
|
||||
expect(screen.getByLabelText("Number")).toHaveAttribute("type", "number");
|
||||
});
|
||||
|
||||
it("handles focus and blur events", async () => {
|
||||
const handleFocus = vi.fn();
|
||||
const handleBlur = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Input label="Username" onFocus={handleFocus} onBlur={handleBlur} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
|
||||
await user.click(input);
|
||||
expect(handleFocus).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.tab();
|
||||
expect(handleBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("accepts a default value", () => {
|
||||
render(<Input label="Username" defaultValue="john_doe" />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
expect(input).toHaveValue("john_doe");
|
||||
});
|
||||
|
||||
it("can be controlled with value prop", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { rerender } = render(
|
||||
<Input label="Username" value="initial" onChange={handleChange} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
expect(input).toHaveValue("initial");
|
||||
|
||||
// Try typing - should call onChange but value stays controlled
|
||||
await user.type(input, "x");
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
|
||||
// Update with new controlled value
|
||||
rerender(
|
||||
<Input label="Username" value="updated" onChange={handleChange} />,
|
||||
);
|
||||
expect(input).toHaveValue("updated");
|
||||
});
|
||||
|
||||
it("supports keyboard navigation", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Input label="Username" />);
|
||||
|
||||
const input = screen.getByLabelText("Username");
|
||||
|
||||
// Tab to focus
|
||||
await user.tab();
|
||||
expect(input).toHaveFocus();
|
||||
|
||||
// Type some text
|
||||
await user.keyboard("test");
|
||||
expect(input).toHaveValue("test");
|
||||
|
||||
// Navigate within text
|
||||
await user.keyboard("{Home}");
|
||||
await user.keyboard("start");
|
||||
expect(input).toHaveValue("starttest");
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { Input } from "./Input";
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Atoms/Tests/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["!autodocs"],
|
||||
args: {
|
||||
onChange: fn(),
|
||||
onFocus: fn(),
|
||||
onBlur: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Test basic input functionality
|
||||
export const BasicInputInteraction: Story = {
|
||||
args: {
|
||||
label: "Username",
|
||||
placeholder: "Enter your username",
|
||||
},
|
||||
play: async function testBasicInput({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByLabelText("Username");
|
||||
|
||||
// Test initial state
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).not.toBeDisabled();
|
||||
expect(input).toHaveValue("");
|
||||
|
||||
// Test typing
|
||||
await userEvent.type(input, "test");
|
||||
expect(input).toHaveValue("test");
|
||||
|
||||
// Test that onChange was called
|
||||
expect(args.onChange).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
// Test input with hidden label
|
||||
export const HiddenLabelInteraction: Story = {
|
||||
args: {
|
||||
label: "Search",
|
||||
placeholder: "Search...",
|
||||
hideLabel: true,
|
||||
},
|
||||
play: async function testHiddenLabel({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByLabelText("Search");
|
||||
|
||||
// Test accessibility with hidden label
|
||||
expect(input).toHaveAccessibleName("Search");
|
||||
expect(input).toHaveAttribute("aria-label", "Search");
|
||||
|
||||
// Test functionality
|
||||
await userEvent.type(input, "query");
|
||||
expect(input).toHaveValue("query");
|
||||
},
|
||||
};
|
||||
|
||||
// Test error state
|
||||
export const ErrorStateInteraction: Story = {
|
||||
args: {
|
||||
label: "Password",
|
||||
placeholder: "Enter password",
|
||||
error: "Password is required",
|
||||
},
|
||||
play: async function testErrorState({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByPlaceholderText("Enter password");
|
||||
const errorMessage = canvas.getByText("Password is required");
|
||||
|
||||
// Test error state rendering
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
|
||||
// Test error styling
|
||||
expect(input).toHaveClass("border-red-500");
|
||||
expect(errorMessage).toHaveClass("!text-red-500");
|
||||
},
|
||||
};
|
||||
|
||||
// Test different input types
|
||||
export const InputTypesInteraction: Story = {
|
||||
render: function renderInputTypes() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Input label="Text Input" type="text" />
|
||||
<Input label="Email Input" type="email" />
|
||||
<Input label="Password Input" type="password" />
|
||||
<Input label="Number Input" type="number" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async function testInputTypes({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test all input types
|
||||
const textInput = canvas.getByLabelText("Text Input");
|
||||
const emailInput = canvas.getByLabelText("Email Input");
|
||||
const passwordInput = canvas.getByLabelText("Password Input");
|
||||
const numberInput = canvas.getByLabelText("Number Input");
|
||||
|
||||
// Test correct types are applied
|
||||
expect(textInput).toHaveAttribute("type", "text");
|
||||
expect(emailInput).toHaveAttribute("type", "email");
|
||||
expect(passwordInput).toHaveAttribute("type", "password");
|
||||
expect(numberInput).toHaveAttribute("type", "number");
|
||||
|
||||
// Test that all inputs are rendered
|
||||
expect(textInput).toBeInTheDocument();
|
||||
expect(emailInput).toBeInTheDocument();
|
||||
expect(passwordInput).toBeInTheDocument();
|
||||
expect(numberInput).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// Test disabled state
|
||||
export const DisabledInteraction: Story = {
|
||||
args: {
|
||||
label: "Disabled Input",
|
||||
placeholder: "This is disabled",
|
||||
disabled: true,
|
||||
},
|
||||
play: async function testDisabledInput({ args, canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByLabelText("Disabled Input");
|
||||
|
||||
// Test disabled state
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveAttribute("disabled");
|
||||
|
||||
// Test that disabled input has empty value and onChange is not called
|
||||
// We don't test typing because disabled inputs prevent user interaction
|
||||
expect(input).toHaveValue("");
|
||||
expect(args.onChange).not.toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
// Test clear input functionality
|
||||
export const ClearInputInteraction: Story = {
|
||||
args: {
|
||||
label: "Clear Test",
|
||||
placeholder: "Type and clear",
|
||||
},
|
||||
play: async function testClearInput({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByLabelText("Clear Test");
|
||||
|
||||
// Type something
|
||||
await userEvent.type(input, "content");
|
||||
expect(input).toHaveValue("content");
|
||||
|
||||
// Clear with select all + delete
|
||||
await userEvent.keyboard("{Control>}a{/Control}");
|
||||
await userEvent.keyboard("{Delete}");
|
||||
expect(input).toHaveValue("");
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { forwardRef } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Link } from "./Link";
|
||||
|
||||
// Mock Next.js Link with proper ref forwarding
|
||||
vi.mock("next/link", () => ({
|
||||
default: forwardRef(function MockNextLink(
|
||||
{ href, children, ...props }: any,
|
||||
ref: any,
|
||||
) {
|
||||
return (
|
||||
<a ref={ref} href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Link Component", () => {
|
||||
it("renders internal link with correct href", () => {
|
||||
render(<Link href="/dashboard">Dashboard</Link>);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Dashboard" });
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute("href", "/dashboard");
|
||||
});
|
||||
|
||||
it("renders external link with target blank", () => {
|
||||
render(
|
||||
<Link href="https://example.com" isExternal>
|
||||
External Link
|
||||
</Link>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "External Link" });
|
||||
expect(link).toHaveAttribute("href", "https://example.com");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("handles click events", async () => {
|
||||
const handleClick = vi.fn((e) => e.preventDefault());
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Link href="/dashboard" onClick={handleClick}>
|
||||
Dashboard
|
||||
</Link>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Dashboard" });
|
||||
await user.click(link);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("supports keyboard navigation", async () => {
|
||||
const handleKeyDown = vi.fn((e) => {
|
||||
if (e.key === "Enter") e.preventDefault();
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Link href="/dashboard" onKeyDown={handleKeyDown}>
|
||||
Dashboard
|
||||
</Link>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Dashboard" });
|
||||
|
||||
await user.tab();
|
||||
expect(link).toHaveFocus();
|
||||
|
||||
await user.keyboard("{Enter}");
|
||||
expect(handleKeyDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<Link href="/dashboard" className="custom-class">
|
||||
Dashboard
|
||||
</Link>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Dashboard" });
|
||||
expect(link).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("forwards ref correctly", () => {
|
||||
const ref = { current: null };
|
||||
|
||||
render(
|
||||
<Link href="/dashboard" ref={ref}>
|
||||
Dashboard
|
||||
</Link>,
|
||||
);
|
||||
|
||||
// Check that ref is populated
|
||||
expect(ref.current).toBeTruthy();
|
||||
});
|
||||
|
||||
it("passes through additional props", () => {
|
||||
render(
|
||||
<Link href="/dashboard" data-testid="custom-link">
|
||||
Dashboard
|
||||
</Link>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Dashboard" });
|
||||
expect(link).toHaveAttribute("data-testid", "custom-link");
|
||||
});
|
||||
|
||||
it("renders children correctly", () => {
|
||||
render(
|
||||
<Link href="/dashboard">
|
||||
<span>Dashboard</span>
|
||||
<span>Icon</span>
|
||||
</Link>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Dashboard")).toBeInTheDocument();
|
||||
expect(screen.getByText("Icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("distinguishes between internal and external links", () => {
|
||||
const { rerender } = render(<Link href="/internal">Internal</Link>);
|
||||
|
||||
let link = screen.getByRole("link", { name: "Internal" });
|
||||
expect(link).not.toHaveAttribute("target");
|
||||
expect(link).not.toHaveAttribute("rel");
|
||||
|
||||
rerender(
|
||||
<Link href="https://external.com" isExternal>
|
||||
External
|
||||
</Link>,
|
||||
);
|
||||
|
||||
link = screen.getByRole("link", { name: "External" });
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
});
|
||||
@@ -2,15 +2,16 @@ import { cn } from "@/lib/utils";
|
||||
import NextLink from "next/link";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
interface LinkProps {
|
||||
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isExternal?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
{ href, children, className, isExternal = false, ...props },
|
||||
{ href, children, className, isExternal = false, title, ...props },
|
||||
ref,
|
||||
) {
|
||||
const linkClasses = cn(
|
||||
@@ -31,6 +32,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={linkClasses}
|
||||
title={title}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Skeleton Component", () => {
|
||||
it("renders skeleton element", () => {
|
||||
render(<Skeleton data-testid="skeleton" />);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
expect(skeleton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<Skeleton className="custom-skeleton" data-testid="skeleton" />);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
expect(skeleton).toHaveClass("custom-skeleton");
|
||||
});
|
||||
|
||||
it("passes through HTML attributes", () => {
|
||||
render(
|
||||
<Skeleton
|
||||
data-testid="skeleton"
|
||||
role="progressbar"
|
||||
aria-label="Loading content"
|
||||
/>,
|
||||
);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
expect(skeleton).toHaveAttribute("role", "progressbar");
|
||||
expect(skeleton).toHaveAttribute("aria-label", "Loading content");
|
||||
});
|
||||
|
||||
it("renders as a div element", () => {
|
||||
render(<Skeleton data-testid="skeleton" />);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
expect(skeleton.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
it("can contain children", () => {
|
||||
render(
|
||||
<Skeleton data-testid="skeleton">
|
||||
<span>Loading...</span>
|
||||
</Skeleton>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports style prop", () => {
|
||||
render(
|
||||
<Skeleton
|
||||
data-testid="skeleton"
|
||||
style={{ width: "100px", height: "20px" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
expect(skeleton).toHaveStyle({ width: "100px", height: "20px" });
|
||||
});
|
||||
|
||||
it("supports onClick handler", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Skeleton data-testid="skeleton" onClick={handleClick} />);
|
||||
|
||||
const skeleton = screen.getByTestId("skeleton");
|
||||
await user.click(skeleton);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@/components/ui/skeleton";
|
||||
@@ -1,183 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { Text, textVariants } from "./Text";
|
||||
|
||||
const meta: Meta<typeof Text> = {
|
||||
title: "Atoms/Tests/Text",
|
||||
component: Text,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["!autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Test basic text rendering
|
||||
export const BasicTextInteraction: Story = {
|
||||
args: {
|
||||
variant: "body",
|
||||
children: "Basic text content",
|
||||
},
|
||||
play: async function testBasicText({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText("Basic text content");
|
||||
|
||||
// Test basic rendering
|
||||
expect(text).toBeInTheDocument();
|
||||
expect(text).toBeVisible();
|
||||
expect(text).toHaveTextContent("Basic text content");
|
||||
},
|
||||
};
|
||||
|
||||
// Test all text variants render correctly
|
||||
export const AllVariantsInteraction: Story = {
|
||||
render: function renderAllVariants() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{textVariants.map((variant) => (
|
||||
<Text key={variant} variant={variant}>
|
||||
{variant}: Sample text for {variant} variant
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async function testAllVariants({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test all variants are rendered
|
||||
for (const variant of textVariants) {
|
||||
const text = canvas.getByText(
|
||||
new RegExp(`${variant}: Sample text for ${variant} variant`),
|
||||
);
|
||||
expect(text).toBeInTheDocument();
|
||||
}
|
||||
|
||||
// Test specific heading elements
|
||||
const h1 = canvas.getByText(/h1: Sample text/);
|
||||
const h2 = canvas.getByText(/h2: Sample text/);
|
||||
const h3 = canvas.getByText(/h3: Sample text/);
|
||||
const h4 = canvas.getByText(/h4: Sample text/);
|
||||
|
||||
expect(h1.tagName).toBe("H1");
|
||||
expect(h2.tagName).toBe("H2");
|
||||
expect(h3.tagName).toBe("H3");
|
||||
expect(h4.tagName).toBe("H4");
|
||||
},
|
||||
};
|
||||
|
||||
// Test custom element override
|
||||
export const CustomElementInteraction: Story = {
|
||||
args: {
|
||||
variant: "body",
|
||||
as: "span",
|
||||
children: "Text as span element",
|
||||
},
|
||||
play: async function testCustomElement({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText("Text as span element");
|
||||
|
||||
// Test custom element type
|
||||
expect(text.tagName).toBe("SPAN");
|
||||
expect(text).toHaveTextContent("Text as span element");
|
||||
},
|
||||
};
|
||||
|
||||
// Test text with custom classes
|
||||
export const CustomClassesInteraction: Story = {
|
||||
args: {
|
||||
variant: "body",
|
||||
children: "Text with custom classes",
|
||||
className: "text-red-500 underline font-bold",
|
||||
},
|
||||
play: async function testCustomClasses({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText("Text with custom classes");
|
||||
|
||||
// Test custom classes are applied
|
||||
expect(text).toHaveClass("text-red-500", "underline", "font-bold");
|
||||
|
||||
// Test variant classes are still present
|
||||
expect(text).toHaveClass("font-sans", "text-sm");
|
||||
},
|
||||
};
|
||||
|
||||
// Test text accessibility
|
||||
export const AccessibilityInteraction: Story = {
|
||||
render: function renderAccessibilityTest() {
|
||||
return (
|
||||
<div>
|
||||
<Text variant="h1" role="heading" aria-level={1}>
|
||||
Main Heading
|
||||
</Text>
|
||||
<Text variant="body">Descriptive text content</Text>
|
||||
<Text variant="small" aria-label="Helper text">
|
||||
SR: Screen reader text
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async function testAccessibility({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test heading accessibility
|
||||
const heading = canvas.getByRole("heading", { level: 1 });
|
||||
expect(heading).toHaveTextContent("Main Heading");
|
||||
|
||||
// Test aria-label
|
||||
const helperText = canvas.getByLabelText("Helper text");
|
||||
expect(helperText).toHaveTextContent("SR: Screen reader text");
|
||||
|
||||
// Test regular text
|
||||
const bodyText = canvas.getByText("Descriptive text content");
|
||||
expect(bodyText).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// Test text selection
|
||||
export const TextSelectionInteraction: Story = {
|
||||
args: {
|
||||
variant: "large",
|
||||
children: "This text can be selected and copied",
|
||||
},
|
||||
play: async function testTextSelection({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText("This text can be selected and copied");
|
||||
|
||||
// Test text selection (simulated)
|
||||
// Note: Actual text selection is limited in test environment
|
||||
await userEvent.click(text);
|
||||
|
||||
// Test double-click for word selection
|
||||
await userEvent.dblClick(text);
|
||||
|
||||
// Verify text is still accessible after interactions
|
||||
expect(text).toBeInTheDocument();
|
||||
expect(text).toHaveTextContent("This text can be selected and copied");
|
||||
},
|
||||
};
|
||||
|
||||
// Test text with HTML content (should be escaped)
|
||||
export const SafeHTMLInteraction: Story = {
|
||||
args: {
|
||||
variant: "body",
|
||||
children: "<script>alert('xss')</script>Safe text content",
|
||||
},
|
||||
play: async function testSafeHTML({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText(
|
||||
"<script>alert('xss')</script>Safe text content",
|
||||
);
|
||||
|
||||
// Test that HTML is escaped/rendered as text
|
||||
expect(text).toHaveTextContent(
|
||||
"<script>alert('xss')</script>Safe text content",
|
||||
);
|
||||
|
||||
// Test that no script elements are created
|
||||
const scripts = canvasElement.querySelectorAll("script");
|
||||
expect(scripts).toHaveLength(0);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Text } from "./Text";
|
||||
|
||||
describe("Text Component", () => {
|
||||
it("renders text content", () => {
|
||||
render(<Text variant="body">Hello World</Text>);
|
||||
|
||||
expect(screen.getByText("Hello World")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with different variants", () => {
|
||||
const { rerender } = render(<Text variant="body">Body Text</Text>);
|
||||
expect(screen.getByText("Body Text")).toBeInTheDocument();
|
||||
|
||||
rerender(<Text variant="body-medium">Medium Text</Text>);
|
||||
expect(screen.getByText("Medium Text")).toBeInTheDocument();
|
||||
|
||||
rerender(<Text variant="small">Small Text</Text>);
|
||||
expect(screen.getByText("Small Text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with custom element using as prop", () => {
|
||||
render(
|
||||
<Text variant="body" as="h1">
|
||||
Heading Text
|
||||
</Text>,
|
||||
);
|
||||
|
||||
const element = screen.getByText("Heading Text");
|
||||
expect(element.tagName).toBe("H1");
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<Text variant="body" className="custom-text">
|
||||
Styled Text
|
||||
</Text>,
|
||||
);
|
||||
|
||||
const element = screen.getByText("Styled Text");
|
||||
expect(element).toHaveClass("custom-text");
|
||||
});
|
||||
|
||||
it("passes through HTML attributes", () => {
|
||||
render(
|
||||
<Text variant="body" data-testid="text-element" title="Tooltip text">
|
||||
Text with attributes
|
||||
</Text>,
|
||||
);
|
||||
|
||||
const element = screen.getByTestId("text-element");
|
||||
expect(element).toHaveAttribute("title", "Tooltip text");
|
||||
});
|
||||
|
||||
it("supports onClick handler", async () => {
|
||||
const handleClick = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Text variant="body" onClick={handleClick}>
|
||||
Clickable Text
|
||||
</Text>,
|
||||
);
|
||||
|
||||
const element = screen.getByText("Clickable Text");
|
||||
await user.click(element);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders children correctly", () => {
|
||||
render(
|
||||
<Text variant="body">
|
||||
<span>Child 1</span>
|
||||
<span>Child 2</span>
|
||||
</Text>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Child 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports different text variants", () => {
|
||||
const variants = [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"lead",
|
||||
"large",
|
||||
"large-medium",
|
||||
"large-semibold",
|
||||
"body",
|
||||
"body-medium",
|
||||
"small",
|
||||
"small-medium",
|
||||
"subtle",
|
||||
];
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const { unmount } = render(
|
||||
<Text variant={variant as any} data-testid={`text-${variant}`}>
|
||||
{variant} text
|
||||
</Text>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(`text-${variant}`)).toBeInTheDocument();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles empty children", () => {
|
||||
render(<Text variant="body" data-testid="empty-text"></Text>);
|
||||
|
||||
const element = screen.getByTestId("empty-text");
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("supports keyboard navigation when interactive", async () => {
|
||||
const handleKeyDown = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Text variant="body" tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
Interactive Text
|
||||
</Text>,
|
||||
);
|
||||
|
||||
const element = screen.getByText("Interactive Text");
|
||||
|
||||
// Focus with tab
|
||||
await user.tab();
|
||||
expect(element).toHaveFocus();
|
||||
|
||||
// Press key
|
||||
await user.keyboard("{Enter}");
|
||||
expect(handleKeyDown).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
25
autogpt_platform/frontend/src/test/setup.ts
Normal file
25
autogpt_platform/frontend/src/test/setup.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock matchMedia for jsdom
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // deprecated
|
||||
removeListener: vi.fn(), // deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock IntersectionObserver
|
||||
(global as any).IntersectionObserver = class IntersectionObserver {
|
||||
constructor() {}
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
};
|
||||
29
autogpt_platform/frontend/vitest.config.mjs
Normal file
29
autogpt_platform/frontend/vitest.config.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
esbuild: {
|
||||
jsx: "automatic",
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
globals: true,
|
||||
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/cypress/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/src/tests/**", // Exclude Playwright tests
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
21
autogpt_platform/frontend/vitest.config.ts
Normal file
21
autogpt_platform/frontend/vitest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference types="vitest" />
|
||||
import path from "path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
name: "chromium",
|
||||
provider: "playwright",
|
||||
headless: true,
|
||||
},
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
globals: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user