mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-12 07:45:14 -05:00
Compare commits
14 Commits
fix/copilo
...
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
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tsc check
|
- name: Run tsc check
|
||||||
run: pnpm type-check
|
run: pnpm types
|
||||||
|
|
||||||
chromatic:
|
chromatic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -148,6 +148,27 @@ jobs:
|
|||||||
onlyChanged: true
|
onlyChanged: true
|
||||||
workingDir: autogpt_platform/frontend
|
workingDir: autogpt_platform/frontend
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -211,7 +232,7 @@ jobs:
|
|||||||
run: pnpm playwright install --with-deps ${{ matrix.browser }}
|
run: pnpm playwright install --with-deps ${{ matrix.browser }}
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- 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
|
- name: Print Final Docker Compose logs
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: tsc
|
- id: tsc
|
||||||
name: Typecheck - AutoGPT Platform - Frontend
|
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/
|
files: ^autogpt_platform/frontend/
|
||||||
types: [file]
|
types: [file]
|
||||||
language: system
|
language: system
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ See `docs/content/platform/getting-started.md` for setup instructions.
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Backend: `poetry run test` (runs pytest with a docker based postgres + prisma).
|
- 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.
|
Always run the relevant linters and tests before committing.
|
||||||
Use conventional commit messages for all commits (e.g. `feat(backend): add API`).
|
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
|
## Repository Overview
|
||||||
|
|
||||||
AutoGPT Platform is a monorepo containing:
|
AutoGPT Platform is a monorepo containing:
|
||||||
|
|
||||||
- **Backend** (`/backend`): Python FastAPI server with async support
|
- **Backend** (`/backend`): Python FastAPI server with async support
|
||||||
- **Frontend** (`/frontend`): Next.js React application
|
- **Frontend** (`/frontend`): Next.js React application
|
||||||
- **Shared Libraries** (`/autogpt_libs`): Common Python utilities
|
- **Shared Libraries** (`/autogpt_libs`): Common Python utilities
|
||||||
@@ -12,6 +13,7 @@ AutoGPT Platform is a monorepo containing:
|
|||||||
## Essential Commands
|
## Essential Commands
|
||||||
|
|
||||||
### Backend Development
|
### Backend Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
cd backend && poetry install
|
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 format # Black + isort
|
||||||
poetry run lint # ruff
|
poetry run lint # ruff
|
||||||
```
|
```
|
||||||
|
|
||||||
More details can be found in TESTING.md
|
More details can be found in TESTING.md
|
||||||
|
|
||||||
#### Creating/Updating Snapshots
|
#### 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.
|
⚠️ **Important**: Always review snapshot changes before committing! Use `git diff` to verify the changes are expected.
|
||||||
|
|
||||||
|
|
||||||
### Frontend Development
|
### Frontend Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
cd frontend && npm install
|
cd frontend && npm install
|
||||||
@@ -67,12 +70,13 @@ npm run storybook
|
|||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Type checking
|
# Type checking
|
||||||
npm run type-check
|
npm run types
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
### Backend Architecture
|
### Backend Architecture
|
||||||
|
|
||||||
- **API Layer**: FastAPI with REST and WebSocket endpoints
|
- **API Layer**: FastAPI with REST and WebSocket endpoints
|
||||||
- **Database**: PostgreSQL with Prisma ORM, includes pgvector for embeddings
|
- **Database**: PostgreSQL with Prisma ORM, includes pgvector for embeddings
|
||||||
- **Queue System**: RabbitMQ for async task processing
|
- **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
|
- **Security**: Cache protection middleware prevents sensitive data caching in browsers/proxies
|
||||||
|
|
||||||
### Frontend Architecture
|
### Frontend Architecture
|
||||||
|
|
||||||
- **Framework**: Next.js App Router with React Server Components
|
- **Framework**: Next.js App Router with React Server Components
|
||||||
- **State Management**: React hooks + Supabase client for real-time updates
|
- **State Management**: React hooks + Supabase client for real-time updates
|
||||||
- **Workflow Builder**: Visual graph editor using @xyflow/react
|
- **Workflow Builder**: Visual graph editor using @xyflow/react
|
||||||
@@ -88,6 +93,7 @@ npm run type-check
|
|||||||
- **Feature Flags**: LaunchDarkly integration
|
- **Feature Flags**: LaunchDarkly integration
|
||||||
|
|
||||||
### Key Concepts
|
### Key Concepts
|
||||||
|
|
||||||
1. **Agent Graphs**: Workflow definitions stored as JSON, executed by the backend
|
1. **Agent Graphs**: Workflow definitions stored as JSON, executed by the backend
|
||||||
2. **Blocks**: Reusable components in `/backend/blocks/` that perform specific tasks
|
2. **Blocks**: Reusable components in `/backend/blocks/` that perform specific tasks
|
||||||
3. **Integrations**: OAuth and API connections stored per user
|
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
|
5. **Virus Scanning**: ClamAV integration for file upload security
|
||||||
|
|
||||||
### Testing Approach
|
### Testing Approach
|
||||||
|
|
||||||
- Backend uses pytest with snapshot testing for API responses
|
- Backend uses pytest with snapshot testing for API responses
|
||||||
- Test files are colocated with source files (`*_test.py`)
|
- Test files are colocated with source files (`*_test.py`)
|
||||||
- Frontend uses Playwright for E2E tests
|
- Frontend uses Playwright for E2E tests
|
||||||
- Component testing via Storybook
|
- Component testing via Storybook
|
||||||
|
|
||||||
### Database Schema
|
### Database Schema
|
||||||
|
|
||||||
Key models (defined in `/backend/schema.prisma`):
|
Key models (defined in `/backend/schema.prisma`):
|
||||||
|
|
||||||
- `User`: Authentication and profile data
|
- `User`: Authentication and profile data
|
||||||
- `AgentGraph`: Workflow definitions with version control
|
- `AgentGraph`: Workflow definitions with version control
|
||||||
- `AgentGraphExecution`: Execution history and results
|
- `AgentGraphExecution`: Execution history and results
|
||||||
@@ -109,6 +118,7 @@ Key models (defined in `/backend/schema.prisma`):
|
|||||||
- `StoreListing`: Marketplace listings for sharing agents
|
- `StoreListing`: Marketplace listings for sharing agents
|
||||||
|
|
||||||
### Environment Configuration
|
### Environment Configuration
|
||||||
|
|
||||||
- Backend: `.env` file in `/backend`
|
- Backend: `.env` file in `/backend`
|
||||||
- Frontend: `.env.local` file in `/frontend`
|
- Frontend: `.env.local` file in `/frontend`
|
||||||
- Both require Supabase credentials and API keys for various services
|
- Both require Supabase credentials and API keys for various services
|
||||||
@@ -116,6 +126,7 @@ Key models (defined in `/backend/schema.prisma`):
|
|||||||
### Common Development Tasks
|
### Common Development Tasks
|
||||||
|
|
||||||
**Adding a new block:**
|
**Adding a new block:**
|
||||||
|
|
||||||
1. Create new file in `/backend/backend/blocks/`
|
1. Create new file in `/backend/backend/blocks/`
|
||||||
2. Inherit from `Block` base class
|
2. Inherit from `Block` base class
|
||||||
3. Define input/output schemas
|
3. Define input/output schemas
|
||||||
@@ -124,12 +135,14 @@ Key models (defined in `/backend/schema.prisma`):
|
|||||||
6. Generate the block uuid using `uuid.uuid4()`
|
6. Generate the block uuid using `uuid.uuid4()`
|
||||||
|
|
||||||
**Modifying the API:**
|
**Modifying the API:**
|
||||||
|
|
||||||
1. Update route in `/backend/backend/server/routers/`
|
1. Update route in `/backend/backend/server/routers/`
|
||||||
2. Add/update Pydantic models in same directory
|
2. Add/update Pydantic models in same directory
|
||||||
3. Write tests alongside the route file
|
3. Write tests alongside the route file
|
||||||
4. Run `poetry run test` to verify
|
4. Run `poetry run test` to verify
|
||||||
|
|
||||||
**Frontend feature development:**
|
**Frontend feature development:**
|
||||||
|
|
||||||
1. Components go in `/frontend/src/components/`
|
1. Components go in `/frontend/src/components/`
|
||||||
2. Use existing UI components from `/frontend/src/components/ui/`
|
2. Use existing UI components from `/frontend/src/components/ui/`
|
||||||
3. Add Storybook stories for new components
|
3. Add Storybook stories for new components
|
||||||
@@ -138,10 +151,11 @@ Key models (defined in `/backend/schema.prisma`):
|
|||||||
### Security Implementation
|
### Security Implementation
|
||||||
|
|
||||||
**Cache Protection Middleware:**
|
**Cache Protection Middleware:**
|
||||||
|
|
||||||
- Located in `/backend/backend/server/middleware/security.py`
|
- 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`
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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-onboarding",
|
||||||
"@storybook/addon-links",
|
"@storybook/addon-links",
|
||||||
"@storybook/addon-docs",
|
"@storybook/addon-docs",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
],
|
],
|
||||||
features: {
|
features: {
|
||||||
experimentalRSC: true,
|
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 start` - Start production server
|
||||||
- `pnpm lint` - Run ESLint and Prettier checks
|
- `pnpm lint` - Run ESLint and Prettier checks
|
||||||
- `pnpm format` - Format code with Prettier
|
- `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` - Run Playwright tests
|
||||||
- `pnpm test-ui` - Run Playwright tests with UI
|
- `pnpm test:ui` - Run Playwright tests with UI
|
||||||
- `pnpm fetch:openapi` - Fetch OpenAPI spec from backend
|
- `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-client` - Generate API client from OpenAPI spec
|
||||||
- `pnpm generate:api-all` - Fetch OpenAPI spec and generate API client
|
- `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:
|
To build a static version of Storybook for deployment, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm build-storybook
|
pnpm storybook:build
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Running Storybook Tests**:
|
3. **Writing Stories**:
|
||||||
Storybook tests can be run using:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test-storybook
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Writing Stories**:
|
|
||||||
Create `.stories.tsx` files alongside your components to define different states and variations of your components.
|
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.
|
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",
|
"start:standalone": "cd .next/standalone && node server.js",
|
||||||
"lint": "next lint && prettier --check .",
|
"lint": "next lint && prettier --check .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"type-check": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
"test": "next build --turbo && playwright test",
|
"test": "next build --turbo && playwright test",
|
||||||
"test-ui": "next build --turbo && playwright test --ui",
|
"test:ui": "next build --turbo && playwright test --ui",
|
||||||
"test:no-build": "playwright test",
|
"test:unit": "vitest --config vitest.config.mjs --run",
|
||||||
|
"test:unit:watch": "vitest --config vitest.config.mjs --watch",
|
||||||
"gentests": "playwright codegen http://localhost:3000",
|
"gentests": "playwright codegen http://localhost:3000",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"storybook:build": "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\"",
|
|
||||||
"fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/app/api/openapi.json && prettier --write ./src/app/api/openapi.json",
|
"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-client": "orval --config ./orval.config.ts",
|
||||||
"generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client"
|
"generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client"
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "9.8.0",
|
"@faker-js/faker": "9.8.0",
|
||||||
"@hookform/resolvers": "5.1.1",
|
"@hookform/resolvers": "5.1.1",
|
||||||
"@next/third-parties": "15.3.3",
|
"@next/third-parties": "15.3.4",
|
||||||
"@phosphor-icons/react": "2.1.10",
|
"@phosphor-icons/react": "2.1.10",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.14",
|
"@radix-ui/react-alert-dialog": "1.1.14",
|
||||||
"@radix-ui/react-avatar": "1.1.10",
|
"@radix-ui/react-avatar": "1.1.10",
|
||||||
@@ -49,13 +48,13 @@
|
|||||||
"@radix-ui/react-tabs": "1.1.12",
|
"@radix-ui/react-tabs": "1.1.12",
|
||||||
"@radix-ui/react-toast": "1.2.14",
|
"@radix-ui/react-toast": "1.2.14",
|
||||||
"@radix-ui/react-tooltip": "1.2.7",
|
"@radix-ui/react-tooltip": "1.2.7",
|
||||||
"@sentry/nextjs": "9.27.0",
|
"@sentry/nextjs": "9.33.0",
|
||||||
"@supabase/ssr": "0.6.1",
|
"@supabase/ssr": "0.6.1",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@types/jaro-winkler": "0.2.4",
|
"@types/jaro-winkler": "0.2.4",
|
||||||
"@xyflow/react": "12.6.4",
|
"@xyflow/react": "12.8.0",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"boring-avatars": "1.11.2",
|
"boring-avatars": "1.11.2",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -63,24 +62,24 @@
|
|||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"cookie": "1.0.2",
|
"cookie": "1.0.2",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.6.0",
|
||||||
"elliptic": "6.6.1",
|
"elliptic": "6.6.1",
|
||||||
"embla-carousel-react": "8.6.0",
|
"embla-carousel-react": "8.6.0",
|
||||||
"framer-motion": "12.16.0",
|
"framer-motion": "12.19.2",
|
||||||
"geist": "1.4.2",
|
"geist": "1.4.2",
|
||||||
"jaro-winkler": "0.2.8",
|
"jaro-winkler": "0.2.8",
|
||||||
"launchdarkly-react-client-sdk": "3.8.1",
|
"launchdarkly-react-client-sdk": "3.8.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucide-react": "0.513.0",
|
"lucide-react": "0.524.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"party-js": "2.2.0",
|
"party-js": "2.2.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-drag-drop-files": "2.4.0",
|
"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-icons": "5.5.0",
|
||||||
"react-markdown": "9.0.3",
|
"react-markdown": "9.0.3",
|
||||||
"react-modal": "3.16.3",
|
"react-modal": "3.16.3",
|
||||||
@@ -91,7 +90,7 @@
|
|||||||
"tailwindcss-animate": "1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"zod": "3.25.56"
|
"zod": "3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "4.0.1",
|
"@chromatic-com/storybook": "4.0.1",
|
||||||
@@ -103,6 +102,9 @@
|
|||||||
"@storybook/nextjs": "9.0.14",
|
"@storybook/nextjs": "9.0.14",
|
||||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||||
"@tanstack/react-query-devtools": "5.81.5",
|
"@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/canvas-confetti": "1.9.0",
|
||||||
"@types/lodash": "4.17.19",
|
"@types/lodash": "4.17.19",
|
||||||
"@types/negotiator": "0.6.4",
|
"@types/negotiator": "0.6.4",
|
||||||
@@ -110,6 +112,7 @@
|
|||||||
"@types/react": "18.3.17",
|
"@types/react": "18.3.17",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-modal": "3.16.3",
|
"@types/react-modal": "3.16.3",
|
||||||
|
"@vitest/browser": "3.2.4",
|
||||||
"axe-playwright": "2.1.0",
|
"axe-playwright": "2.1.0",
|
||||||
"chromatic": "11.25.2",
|
"chromatic": "11.25.2",
|
||||||
"concurrently": "9.2.0",
|
"concurrently": "9.2.0",
|
||||||
@@ -118,6 +121,7 @@
|
|||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"eslint-plugin-storybook": "9.0.14",
|
"eslint-plugin-storybook": "9.0.14",
|
||||||
"import-in-the-middle": "1.14.2",
|
"import-in-the-middle": "1.14.2",
|
||||||
|
"jsdom": "26.1.0",
|
||||||
"msw": "2.10.2",
|
"msw": "2.10.2",
|
||||||
"msw-storybook-addon": "2.0.5",
|
"msw-storybook-addon": "2.0.5",
|
||||||
"orval": "7.10.0",
|
"orval": "7.10.0",
|
||||||
@@ -127,7 +131,9 @@
|
|||||||
"require-in-the-middle": "7.5.2",
|
"require-in-the-middle": "7.5.2",
|
||||||
"storybook": "9.0.14",
|
"storybook": "9.0.14",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3",
|
||||||
|
"vite": "7.0.0",
|
||||||
|
"vitest": "3.2.4"
|
||||||
},
|
},
|
||||||
"msw": {
|
"msw": {
|
||||||
"workerDirectory": [
|
"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 { render, screen } from "@testing-library/react";
|
||||||
// import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
// import { Badge } from "./Badge";
|
import { Badge } from "./Badge";
|
||||||
|
|
||||||
// describe("Badge Component", () => {
|
describe("Badge Component", () => {
|
||||||
// it("renders badge with content", () => {
|
it("renders badge with content", () => {
|
||||||
// render(<Badge variant="success">Success</Badge>);
|
render(<Badge variant="success">Success</Badge>);
|
||||||
|
|
||||||
// expect(screen.getByText("Success")).toBeInTheDocument();
|
expect(screen.getByText("Success")).toBeInTheDocument();
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("applies correct variant styles", () => {
|
it("applies correct variant styles", () => {
|
||||||
// const { rerender } = render(<Badge variant="success">Success</Badge>);
|
const { rerender } = render(<Badge variant="success">Success</Badge>);
|
||||||
// let badge = screen.getByText("Success");
|
let badge = screen.getByText("Success");
|
||||||
// expect(badge).toHaveClass("bg-green-100", "text-green-800");
|
expect(badge).toHaveClass("bg-green-100", "text-green-800");
|
||||||
|
|
||||||
// rerender(<Badge variant="error">Error</Badge>);
|
rerender(<Badge variant="error">Error</Badge>);
|
||||||
// badge = screen.getByText("Error");
|
badge = screen.getByText("Error");
|
||||||
// expect(badge).toHaveClass("bg-red-100", "text-red-800");
|
expect(badge).toHaveClass("bg-red-100", "text-red-800");
|
||||||
|
|
||||||
// rerender(<Badge variant="info">Info</Badge>);
|
rerender(<Badge variant="info">Info</Badge>);
|
||||||
// badge = screen.getByText("Info");
|
badge = screen.getByText("Info");
|
||||||
// expect(badge).toHaveClass("bg-slate-100", "text-slate-800");
|
expect(badge).toHaveClass("bg-slate-100", "text-slate-800");
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("applies custom className", () => {
|
it("applies custom className", () => {
|
||||||
// render(
|
render(
|
||||||
// <Badge variant="success" className="custom-class">
|
<Badge variant="success" className="custom-class">
|
||||||
// Success
|
Success
|
||||||
// </Badge>,
|
</Badge>,
|
||||||
// );
|
);
|
||||||
|
|
||||||
// const badge = screen.getByText("Success");
|
const badge = screen.getByText("Success");
|
||||||
// expect(badge).toHaveClass("custom-class");
|
expect(badge).toHaveClass("custom-class");
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("renders as span element", () => {
|
it("renders as span element", () => {
|
||||||
// render(<Badge variant="success">Success</Badge>);
|
render(<Badge variant="success">Success</Badge>);
|
||||||
|
|
||||||
// const badge = screen.getByText("Success");
|
const badge = screen.getByText("Success");
|
||||||
// expect(badge.tagName).toBe("SPAN");
|
expect(badge.tagName).toBe("SPAN");
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("renders children correctly", () => {
|
it("renders children correctly", () => {
|
||||||
// render(
|
render(
|
||||||
// <Badge variant="success">
|
<Badge variant="success">
|
||||||
// <span>Custom</span> Content
|
<span>Custom</span> Content
|
||||||
// </Badge>,
|
</Badge>,
|
||||||
// );
|
);
|
||||||
|
|
||||||
// expect(screen.getByText("Custom")).toBeInTheDocument();
|
expect(screen.getByText("Custom")).toBeInTheDocument();
|
||||||
// expect(screen.getByText("Content")).toBeInTheDocument();
|
expect(screen.getByText("Content")).toBeInTheDocument();
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("supports all badge variants", () => {
|
it("supports all badge variants", () => {
|
||||||
// const variants = ["success", "error", "info"] as const;
|
const variants = ["success", "error", "info"] as const;
|
||||||
|
|
||||||
// variants.forEach((variant) => {
|
variants.forEach((variant) => {
|
||||||
// const { unmount } = render(
|
const { unmount } = render(
|
||||||
// <Badge variant={variant} data-testid={`badge-${variant}`}>
|
<Badge variant={variant} data-testid={`badge-${variant}`}>
|
||||||
// {variant}
|
{variant}
|
||||||
// </Badge>,
|
</Badge>,
|
||||||
// );
|
);
|
||||||
|
|
||||||
// expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument();
|
expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument();
|
||||||
// unmount();
|
unmount();
|
||||||
// });
|
});
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("handles long text content", () => {
|
it("handles long text content", () => {
|
||||||
// render(
|
render(
|
||||||
// <Badge variant="info">
|
<Badge variant="info">
|
||||||
// Very long text that should be handled properly by the component
|
Very long text that should be handled properly by the component
|
||||||
// </Badge>,
|
</Badge>,
|
||||||
// );
|
);
|
||||||
|
|
||||||
// const badge = screen.getByText(/Very long text/);
|
const badge = screen.getByText(/Very long text/);
|
||||||
// expect(badge).toBeInTheDocument();
|
expect(badge).toBeInTheDocument();
|
||||||
// expect(badge).toHaveClass("overflow-hidden", "text-ellipsis");
|
expect(badge).toHaveClass("overflow-hidden", "text-ellipsis");
|
||||||
// });
|
});
|
||||||
// });
|
});
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
type BadgeVariant = "success" | "error" | "info";
|
type BadgeVariant = "success" | "error" | "info";
|
||||||
|
|
||||||
interface BadgeProps {
|
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
variant: BadgeVariant;
|
variant: BadgeVariant;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const badgeVariants: Record<BadgeVariant, string> = {
|
const badgeVariants: Record<BadgeVariant, string> = {
|
||||||
@@ -14,7 +13,7 @@ const badgeVariants: Record<BadgeVariant, string> = {
|
|||||||
info: "bg-slate-100 text-slate-800",
|
info: "bg-slate-100 text-slate-800",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Badge({ variant, children, className }: BadgeProps) {
|
export function Badge({ variant, children, className, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -28,6 +27,7 @@ export function Badge({ variant, children, className }: BadgeProps) {
|
|||||||
badgeVariants[variant],
|
badgeVariants[variant],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</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 NextLink from "next/link";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
interface LinkProps {
|
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
href: string;
|
href: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
isExternal?: boolean;
|
isExternal?: boolean;
|
||||||
variant?: "primary" | "secondary";
|
variant?: "primary" | "secondary";
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||||
@@ -16,6 +17,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
isExternal = false,
|
isExternal = false,
|
||||||
|
title,
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
@@ -40,6 +42,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={linkClasses}
|
className={linkClasses}
|
||||||
|
title={title}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{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:
|
If you want to run the tests in a UI where you can identify each locator used you can use the following command:
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
pnpm test --debug
|
pnpm test --debug
|
||||||
|
|||||||
Reference in New Issue
Block a user