mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-12 16:48:06 -05:00
Compare commits
14 Commits
dev
...
chore/stor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c9277f63 | ||
|
|
0fd3c2daae | ||
|
|
8b0888b5aa | ||
|
|
0961aed731 | ||
|
|
b272b79652 | ||
|
|
cb9fda0f1d | ||
|
|
1ed1af8ca0 | ||
|
|
0882a277b1 | ||
|
|
d29f086dec | ||
|
|
2c6b9c7c27 | ||
|
|
1029ee5c45 | ||
|
|
4e817c8d8a | ||
|
|
7c3e8ec221 | ||
|
|
996103d1e1 |
25
.github/workflows/platform-frontend-ci.yml
vendored
25
.github/workflows/platform-frontend-ci.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tsc check
|
||||
run: pnpm type-check
|
||||
run: pnpm types
|
||||
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -148,6 +148,27 @@ jobs:
|
||||
onlyChanged: true
|
||||
workingDir: autogpt_platform/frontend
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
buildScriptName: storybook:build
|
||||
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "21"
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run unit tests
|
||||
run: pnpm test:unit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -211,7 +232,7 @@ jobs:
|
||||
run: pnpm playwright install --with-deps ${{ matrix.browser }}
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm test:no-build --project=${{ matrix.browser }}
|
||||
run: pnpm playwright test --project=${{ matrix.browser }}
|
||||
|
||||
- name: Print Final Docker Compose logs
|
||||
if: always()
|
||||
|
||||
@@ -235,7 +235,7 @@ repos:
|
||||
hooks:
|
||||
- id: tsc
|
||||
name: Typecheck - AutoGPT Platform - Frontend
|
||||
entry: bash -c 'cd autogpt_platform/frontend && pnpm type-check'
|
||||
entry: bash -c 'cd autogpt_platform/frontend && pnpm types'
|
||||
files: ^autogpt_platform/frontend/
|
||||
types: [file]
|
||||
language: system
|
||||
|
||||
@@ -19,7 +19,7 @@ See `docs/content/platform/getting-started.md` for setup instructions.
|
||||
## Testing
|
||||
|
||||
- Backend: `poetry run test` (runs pytest with a docker based postgres + prisma).
|
||||
- Frontend: `pnpm test` or `pnpm test-ui` for Playwright tests. See `docs/content/platform/contributing/tests.md` for tips.
|
||||
- Frontend: `pnpm test` or `pnpm test:ui` for Playwright tests. See `docs/content/platform/contributing/tests.md` for tips.
|
||||
|
||||
Always run the relevant linters and tests before committing.
|
||||
Use conventional commit messages for all commits (e.g. `feat(backend): add API`).
|
||||
|
||||
@@ -5,6 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Repository Overview
|
||||
|
||||
AutoGPT Platform is a monorepo containing:
|
||||
|
||||
- **Backend** (`/backend`): Python FastAPI server with async support
|
||||
- **Frontend** (`/frontend`): Next.js React application
|
||||
- **Shared Libraries** (`/autogpt_libs`): Common Python utilities
|
||||
@@ -12,6 +13,7 @@ AutoGPT Platform is a monorepo containing:
|
||||
## Essential Commands
|
||||
|
||||
### Backend Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd backend && poetry install
|
||||
@@ -36,6 +38,7 @@ poetry run pytest path/to/test_file.py::test_function_name
|
||||
poetry run format # Black + isort
|
||||
poetry run lint # ruff
|
||||
```
|
||||
|
||||
More details can be found in TESTING.md
|
||||
|
||||
#### Creating/Updating Snapshots
|
||||
@@ -48,8 +51,8 @@ poetry run pytest path/to/test.py --snapshot-update
|
||||
|
||||
⚠️ **Important**: Always review snapshot changes before committing! Use `git diff` to verify the changes are expected.
|
||||
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd frontend && npm install
|
||||
@@ -67,12 +70,13 @@ npm run storybook
|
||||
npm run build
|
||||
|
||||
# Type checking
|
||||
npm run type-check
|
||||
npm run types
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
- **API Layer**: FastAPI with REST and WebSocket endpoints
|
||||
- **Database**: PostgreSQL with Prisma ORM, includes pgvector for embeddings
|
||||
- **Queue System**: RabbitMQ for async task processing
|
||||
@@ -81,6 +85,7 @@ npm run type-check
|
||||
- **Security**: Cache protection middleware prevents sensitive data caching in browsers/proxies
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
- **Framework**: Next.js App Router with React Server Components
|
||||
- **State Management**: React hooks + Supabase client for real-time updates
|
||||
- **Workflow Builder**: Visual graph editor using @xyflow/react
|
||||
@@ -88,6 +93,7 @@ npm run type-check
|
||||
- **Feature Flags**: LaunchDarkly integration
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **Agent Graphs**: Workflow definitions stored as JSON, executed by the backend
|
||||
2. **Blocks**: Reusable components in `/backend/blocks/` that perform specific tasks
|
||||
3. **Integrations**: OAuth and API connections stored per user
|
||||
@@ -95,13 +101,16 @@ npm run type-check
|
||||
5. **Virus Scanning**: ClamAV integration for file upload security
|
||||
|
||||
### Testing Approach
|
||||
|
||||
- Backend uses pytest with snapshot testing for API responses
|
||||
- Test files are colocated with source files (`*_test.py`)
|
||||
- Frontend uses Playwright for E2E tests
|
||||
- Component testing via Storybook
|
||||
|
||||
### Database Schema
|
||||
|
||||
Key models (defined in `/backend/schema.prisma`):
|
||||
|
||||
- `User`: Authentication and profile data
|
||||
- `AgentGraph`: Workflow definitions with version control
|
||||
- `AgentGraphExecution`: Execution history and results
|
||||
@@ -109,6 +118,7 @@ Key models (defined in `/backend/schema.prisma`):
|
||||
- `StoreListing`: Marketplace listings for sharing agents
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
- Backend: `.env` file in `/backend`
|
||||
- Frontend: `.env.local` file in `/frontend`
|
||||
- Both require Supabase credentials and API keys for various services
|
||||
@@ -116,6 +126,7 @@ Key models (defined in `/backend/schema.prisma`):
|
||||
### Common Development Tasks
|
||||
|
||||
**Adding a new block:**
|
||||
|
||||
1. Create new file in `/backend/backend/blocks/`
|
||||
2. Inherit from `Block` base class
|
||||
3. Define input/output schemas
|
||||
@@ -124,12 +135,14 @@ Key models (defined in `/backend/schema.prisma`):
|
||||
6. Generate the block uuid using `uuid.uuid4()`
|
||||
|
||||
**Modifying the API:**
|
||||
|
||||
1. Update route in `/backend/backend/server/routers/`
|
||||
2. Add/update Pydantic models in same directory
|
||||
3. Write tests alongside the route file
|
||||
4. Run `poetry run test` to verify
|
||||
|
||||
**Frontend feature development:**
|
||||
|
||||
1. Components go in `/frontend/src/components/`
|
||||
2. Use existing UI components from `/frontend/src/components/ui/`
|
||||
3. Add Storybook stories for new components
|
||||
@@ -138,10 +151,11 @@ Key models (defined in `/backend/schema.prisma`):
|
||||
### Security Implementation
|
||||
|
||||
**Cache Protection Middleware:**
|
||||
|
||||
- Located in `/backend/backend/server/middleware/security.py`
|
||||
- Default behavior: Disables caching for ALL endpoints with `Cache-Control: no-store, no-cache, must-revalidate, private`
|
||||
- Uses an allow list approach - only explicitly permitted paths can be cached
|
||||
- Cacheable paths include: static assets (`/static/*`, `/_next/static/*`), health checks, public store pages, documentation
|
||||
- Prevents sensitive data (auth tokens, API keys, user data) from being cached by browsers/proxies
|
||||
- To allow caching for a new endpoint, add it to `CACHEABLE_PATHS` in the middleware
|
||||
- Applied to both main API server and external API applications
|
||||
- Applied to both main API server and external API applications
|
||||
|
||||
@@ -13,6 +13,7 @@ const config: StorybookConfig = {
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
features: {
|
||||
experimentalRSC: true,
|
||||
|
||||
@@ -60,10 +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 fetch:openapi` - Fetch OpenAPI spec from backend
|
||||
- `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
|
||||
- `pnpm generate:api-client` - Generate API client from OpenAPI spec
|
||||
- `pnpm generate:api-all` - Fetch OpenAPI spec and generate API client
|
||||
|
||||
@@ -237,17 +238,10 @@ 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 storybook:build
|
||||
```
|
||||
|
||||
3. **Running Storybook Tests**:
|
||||
Storybook tests can be run using:
|
||||
|
||||
```bash
|
||||
pnpm test-storybook
|
||||
```
|
||||
|
||||
4. **Writing Stories**:
|
||||
3. **Writing Stories**:
|
||||
Create `.stories.tsx` files alongside your components to define different states and variations of your components.
|
||||
|
||||
By integrating Storybook into our development workflow, we can streamline UI development, improve component reusability, and maintain a consistent design system across the project.
|
||||
|
||||
@@ -9,15 +9,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",
|
||||
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"",
|
||||
"storybook:build": "storybook build",
|
||||
"fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/app/api/openapi.json && prettier --write ./src/app/api/openapi.json",
|
||||
"generate:api-client": "orval --config ./orval.config.ts",
|
||||
"generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client"
|
||||
@@ -28,7 +27,7 @@
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "9.8.0",
|
||||
"@hookform/resolvers": "5.1.1",
|
||||
"@next/third-parties": "15.3.3",
|
||||
"@next/third-parties": "15.3.4",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@radix-ui/react-alert-dialog": "1.1.14",
|
||||
"@radix-ui/react-avatar": "1.1.10",
|
||||
@@ -49,13 +48,13 @@
|
||||
"@radix-ui/react-tabs": "1.1.12",
|
||||
"@radix-ui/react-toast": "1.2.14",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@sentry/nextjs": "9.27.0",
|
||||
"@sentry/nextjs": "9.33.0",
|
||||
"@supabase/ssr": "0.6.1",
|
||||
"@supabase/supabase-js": "2.50.0",
|
||||
"@tanstack/react-query": "5.80.7",
|
||||
"@supabase/supabase-js": "2.50.2",
|
||||
"@tanstack/react-query": "5.81.2",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@types/jaro-winkler": "0.2.4",
|
||||
"@xyflow/react": "12.6.4",
|
||||
"@xyflow/react": "12.8.0",
|
||||
"ajv": "8.17.1",
|
||||
"boring-avatars": "1.11.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -63,24 +62,24 @@
|
||||
"cmdk": "1.1.1",
|
||||
"cookie": "1.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
"dotenv": "16.5.0",
|
||||
"dotenv": "16.6.0",
|
||||
"elliptic": "6.6.1",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"framer-motion": "12.16.0",
|
||||
"framer-motion": "12.19.2",
|
||||
"geist": "1.4.2",
|
||||
"jaro-winkler": "0.2.8",
|
||||
"launchdarkly-react-client-sdk": "3.8.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.513.0",
|
||||
"lucide-react": "0.524.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.3.3",
|
||||
"next": "15.3.4",
|
||||
"next-themes": "0.4.6",
|
||||
"party-js": "2.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-drag-drop-files": "2.4.0",
|
||||
"react-hook-form": "7.57.0",
|
||||
"react-hook-form": "7.58.1",
|
||||
"react-icons": "5.5.0",
|
||||
"react-markdown": "9.0.3",
|
||||
"react-modal": "3.16.3",
|
||||
@@ -91,7 +90,7 @@
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"uuid": "11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
"zod": "3.25.56"
|
||||
"zod": "3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
@@ -103,6 +102,9 @@
|
||||
"@storybook/nextjs": "9.0.14",
|
||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||
"@tanstack/react-query-devtools": "5.81.5",
|
||||
"@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.19",
|
||||
"@types/negotiator": "0.6.4",
|
||||
@@ -110,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.2.0",
|
||||
@@ -118,6 +121,7 @@
|
||||
"eslint-config-next": "15.3.4",
|
||||
"eslint-plugin-storybook": "9.0.14",
|
||||
"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",
|
||||
@@ -127,7 +131,9 @@
|
||||
"require-in-the-middle": "7.5.2",
|
||||
"storybook": "9.0.14",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.0",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
1510
autogpt_platform/frontend/pnpm-lock.yaml
generated
1510
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,81 +1,81 @@
|
||||
// import { render, screen } from "@testing-library/react";
|
||||
// import { describe, expect, it } from "vitest";
|
||||
// import { Badge } from "./Badge";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
// describe("Badge Component", () => {
|
||||
// it("renders badge with content", () => {
|
||||
// render(<Badge variant="success">Success</Badge>);
|
||||
describe("Badge Component", () => {
|
||||
it("renders badge with content", () => {
|
||||
render(<Badge variant="success">Success</Badge>);
|
||||
|
||||
// expect(screen.getByText("Success")).toBeInTheDocument();
|
||||
// });
|
||||
expect(screen.getByText("Success")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// it("applies correct variant styles", () => {
|
||||
// const { rerender } = render(<Badge variant="success">Success</Badge>);
|
||||
// let badge = screen.getByText("Success");
|
||||
// expect(badge).toHaveClass("bg-green-100", "text-green-800");
|
||||
it("applies correct variant styles", () => {
|
||||
const { rerender } = render(<Badge variant="success">Success</Badge>);
|
||||
let badge = screen.getByText("Success");
|
||||
expect(badge).toHaveClass("bg-green-100", "text-green-800");
|
||||
|
||||
// rerender(<Badge variant="error">Error</Badge>);
|
||||
// badge = screen.getByText("Error");
|
||||
// expect(badge).toHaveClass("bg-red-100", "text-red-800");
|
||||
rerender(<Badge variant="error">Error</Badge>);
|
||||
badge = screen.getByText("Error");
|
||||
expect(badge).toHaveClass("bg-red-100", "text-red-800");
|
||||
|
||||
// rerender(<Badge variant="info">Info</Badge>);
|
||||
// badge = screen.getByText("Info");
|
||||
// expect(badge).toHaveClass("bg-slate-100", "text-slate-800");
|
||||
// });
|
||||
rerender(<Badge variant="info">Info</Badge>);
|
||||
badge = screen.getByText("Info");
|
||||
expect(badge).toHaveClass("bg-slate-100", "text-slate-800");
|
||||
});
|
||||
|
||||
// it("applies custom className", () => {
|
||||
// render(
|
||||
// <Badge variant="success" className="custom-class">
|
||||
// Success
|
||||
// </Badge>,
|
||||
// );
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<Badge variant="success" className="custom-class">
|
||||
Success
|
||||
</Badge>,
|
||||
);
|
||||
|
||||
// const badge = screen.getByText("Success");
|
||||
// expect(badge).toHaveClass("custom-class");
|
||||
// });
|
||||
const badge = screen.getByText("Success");
|
||||
expect(badge).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
// it("renders as span element", () => {
|
||||
// render(<Badge variant="success">Success</Badge>);
|
||||
it("renders as span element", () => {
|
||||
render(<Badge variant="success">Success</Badge>);
|
||||
|
||||
// const badge = screen.getByText("Success");
|
||||
// expect(badge.tagName).toBe("SPAN");
|
||||
// });
|
||||
const badge = screen.getByText("Success");
|
||||
expect(badge.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
// it("renders children correctly", () => {
|
||||
// render(
|
||||
// <Badge variant="success">
|
||||
// <span>Custom</span> Content
|
||||
// </Badge>,
|
||||
// );
|
||||
it("renders children correctly", () => {
|
||||
render(
|
||||
<Badge variant="success">
|
||||
<span>Custom</span> Content
|
||||
</Badge>,
|
||||
);
|
||||
|
||||
// expect(screen.getByText("Custom")).toBeInTheDocument();
|
||||
// expect(screen.getByText("Content")).toBeInTheDocument();
|
||||
// });
|
||||
expect(screen.getByText("Custom")).toBeInTheDocument();
|
||||
expect(screen.getByText("Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// it("supports all badge variants", () => {
|
||||
// const variants = ["success", "error", "info"] as const;
|
||||
it("supports all badge variants", () => {
|
||||
const variants = ["success", "error", "info"] as const;
|
||||
|
||||
// variants.forEach((variant) => {
|
||||
// const { unmount } = render(
|
||||
// <Badge variant={variant} data-testid={`badge-${variant}`}>
|
||||
// {variant}
|
||||
// </Badge>,
|
||||
// );
|
||||
variants.forEach((variant) => {
|
||||
const { unmount } = render(
|
||||
<Badge variant={variant} data-testid={`badge-${variant}`}>
|
||||
{variant}
|
||||
</Badge>,
|
||||
);
|
||||
|
||||
// expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument();
|
||||
// unmount();
|
||||
// });
|
||||
// });
|
||||
expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
// it("handles long text content", () => {
|
||||
// render(
|
||||
// <Badge variant="info">
|
||||
// Very long text that should be handled properly by the component
|
||||
// </Badge>,
|
||||
// );
|
||||
it("handles long text content", () => {
|
||||
render(
|
||||
<Badge variant="info">
|
||||
Very long text that should be handled properly by the component
|
||||
</Badge>,
|
||||
);
|
||||
|
||||
// const badge = screen.getByText(/Very long text/);
|
||||
// expect(badge).toBeInTheDocument();
|
||||
// expect(badge).toHaveClass("overflow-hidden", "text-ellipsis");
|
||||
// });
|
||||
// });
|
||||
const badge = screen.getByText(/Very long text/);
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge).toHaveClass("overflow-hidden", "text-ellipsis");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,9 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
type BadgeVariant = "success" | "error" | "info";
|
||||
|
||||
interface BadgeProps {
|
||||
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
variant: BadgeVariant;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const badgeVariants: Record<BadgeVariant, string> = {
|
||||
@@ -14,7 +13,7 @@ const badgeVariants: Record<BadgeVariant, string> = {
|
||||
info: "bg-slate-100 text-slate-800",
|
||||
};
|
||||
|
||||
export function Badge({ variant, children, className }: BadgeProps) {
|
||||
export function Badge({ variant, children, className, ...props }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -28,6 +27,7 @@ export function Badge({ variant, children, className }: BadgeProps) {
|
||||
badgeVariants[variant],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,12 +2,13 @@ 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;
|
||||
variant?: "primary" | "secondary";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
@@ -16,6 +17,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
children,
|
||||
className,
|
||||
isExternal = false,
|
||||
title,
|
||||
variant = "primary",
|
||||
...props
|
||||
},
|
||||
@@ -40,6 +42,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";
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -19,10 +19,10 @@ pnpm test
|
||||
If you want to run the tests in a UI where you can identify each locator used you can use the following command:
|
||||
|
||||
```bash
|
||||
pnpm test-ui
|
||||
pnpm test:ui
|
||||
```
|
||||
|
||||
You can also pass `--debug` to the test command to open the browsers in view mode rather than headless. This works with both the `pnpm test` and `pnpm test-ui` commands.
|
||||
You can also pass `--debug` to the test command to open the browsers in view mode rather than headless. This works with both the `pnpm test` and `pnpm test:ui` commands.
|
||||
|
||||
```bash
|
||||
pnpm test --debug
|
||||
|
||||
Reference in New Issue
Block a user