test: Add Vitest configuration and initial tests for the DexScreener connect form.

This commit is contained in:
API Test Bot 2026-02-01 15:05:19 +07:00
parent 828d7d695a
commit fd9eddf7fa
11 changed files with 1480 additions and 116 deletions

View file

@ -38,3 +38,75 @@ Backend của SurfSense là một ứng dụng **Python FastAPI** mạnh mẽ,
- Nếu là tác vụ AI: Đẩy vào LangGraph runner để streaming phản hồi.
- Nếu là tác vụ dài (Ingestion): Đẩy job vào Redis queue cho Celery.
5. **Response**: Trả về JSON hoặc Streaming Response (SSE).
## Critical RAG Pipeline Fix (Feb 2026)
### DexScreener Connector Integration
**Issue Discovered**: DexScreener connector was successfully implemented and indexed data into `search_space_id = 7`, but the LLM could not retrieve this data when users asked about crypto prices.
**Root Cause**: Missing connector mapping in `_CONNECTOR_TYPE_TO_SEARCHABLE` dictionary.
**File**: `surfsense_backend/app/agents/new_chat/chat_deepagent.py`
**The Problem**:
```python
# BEFORE (Missing mapping)
_CONNECTOR_TYPE_TO_SEARCHABLE = {
"GMAIL": "GMAIL",
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
"SLACK_CONNECTOR": "SLACK",
# ... other connectors ...
# ❌ DEXSCREENER_CONNECTOR was MISSING
}
```
**Impact**:
1. `connector_service.get_available_connectors()` returned DexScreener connector type
2. `_map_connectors_to_searchable_types()` could not find mapping → ignored DexScreener
3. LLM's tool description didn't mention DexScreener as available
4. LLM never searched DexScreener data, responded "can't see price data"
**The Fix**:
```python
# AFTER (Fixed)
_CONNECTOR_TYPE_TO_SEARCHABLE = {
"GMAIL": "GMAIL",
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
"SLACK_CONNECTOR": "SLACK",
# ... other connectors ...
"DEXSCREENER_CONNECTOR": "DEXSCREENER_CONNECTOR", # ✅ Added
}
```
**Verification**:
- User query: *"What's the current price of WETH?"*
- LLM successfully retrieved: ~$2,442 USD with DexScreener citations
- Citations linked to indexed trading pairs with metadata (chain, DEX, liquidity, volume)
**Lesson Learned**: When adding new connectors, **ALWAYS** update the `_CONNECTOR_TYPE_TO_SEARCHABLE` mapping to enable RAG retrieval. This is a critical step that's easy to miss during implementation.
---
## Connector Architecture Pattern
### Adding New Connectors (Best Practices)
Khi thêm connector mới, cần update **4 locations**:
1. **Connector Class** (`app/connectors/`)
- Implement data fetching logic
- Format data to markdown for indexing
2. **Database Enum** (`app/db.py`)
- Add to `SearchSourceConnectorType` enum
3. **API Routes** (`app/routes/`)
- Create add/delete/test endpoints
4. **RAG Mapping** (`app/agents/new_chat/chat_deepagent.py`) ⚠️ **CRITICAL**
- Add to `_CONNECTOR_TYPE_TO_SEARCHABLE` dictionary
- **Failure to do this = LLM cannot access connector data**
---

View file

@ -18,8 +18,9 @@
- 🎫 **Jira** - Tìm kiếm tickets
- 📚 **Confluence** - Tìm kiếm wiki pages
- 🗂️ **Microsoft Teams** - Tìm kiếm chats và files
- 💰 **DexScreener** - Theo dõi giá token crypto và trading pairs
**Tổng cộng:** SurfSense hỗ trợ **26+ connectors** khác nhau!
**Tổng cộng:** SurfSense hỗ trợ **27+ connectors** khác nhau!
---
@ -238,6 +239,30 @@ User → SurfSense → Notion OAuth → Access Token → SurfSense
- Chỉ hoạt động khi SurfSense chạy self-hosted
- Truy cập trực tiếp vào local file system
### 5. API-Based (No Authentication)
**Ví dụ:** DexScreener Connector
- Không cần OAuth hay API key (public API)
- User chỉ cần cấu hình tokens muốn theo dõi
- Ưu điểm:
- Setup cực kỳ đơn giản (không cần đăng ký API key)
- Miễn phí hoàn toàn
- Real-time data từ public blockchain
- Nhược điểm:
- Bị giới hạn rate limit của public API
- Không có personalized data
**Flow:**
```
User → Nhập token addresses → SurfSense → DexScreener Public API → Token Price Data
```
**Use Case:**
- Theo dõi giá crypto tokens (WETH, USDC, etc.)
- Phân tích trading pairs trên các DEX
- AI có thể trả lời: *"What's the current price of WETH?"*
---
## 🛠️ Cấu Hình Connector
@ -278,6 +303,24 @@ Mỗi connector có các settings:
}
```
**DexScreener:**
```json
{
"tokens": [
{
"chain": "ethereum",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"name": "WETH"
},
{
"chain": "bsc",
"address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"name": "WBNB"
}
]
}
```
---
## 💡 Use Cases
@ -320,6 +363,23 @@ Mỗi connector có các settings:
3. Slack discussion về issue
4. Confluence: Payment API Architecture
### 3. Crypto Trader
**Scenario:** Theo dõi giá token và phân tích market trends.
**Connectors kết nối:**
- DexScreener (token prices và trading pairs)
- Twitter/X (crypto news - nếu có connector)
- Notion (trading notes)
**Search query trong AI Chat:** *"What's the current price of WETH and how has it changed in the last 24 hours?"*
**Kết quả:**
- AI trả lời với real-time price data từ DexScreener
- Hiển thị price changes (5m, 1h, 24h)
- Liquidity và volume information
- Citations link đến DexScreener pairs
---
## 🚨 Lưu Ý Quan Trọng

View file

@ -796,6 +796,76 @@ async def index_connector_data(connector: SearchSourceConnector):
# ... other connector types
```
---
## ⚠️ CRITICAL: Enable RAG Retrieval
**This is the most commonly missed step when adding new connectors!**
### The Problem
Even if your connector successfully:
1. ✅ Stores data in the database
2. ✅ Indexes data into vector store
3. ✅ Shows up in the UI
The LLM **WILL NOT** be able to retrieve this data unless you add the connector to the RAG mapping.
### The Fix
**File:** `surfsense_backend/app/agents/new_chat/chat_deepagent.py`
**Add your connector to `_CONNECTOR_TYPE_TO_SEARCHABLE`:**
```python
_CONNECTOR_TYPE_TO_SEARCHABLE = {
"GMAIL": "GMAIL",
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
"SLACK_CONNECTOR": "SLACK",
"NOTION_CONNECTOR": "NOTION",
# ... other connectors ...
# ✅ ADD YOUR NEW CONNECTOR HERE
"DEXSCREENER_CONNECTOR": "DEXSCREENER_CONNECTOR",
"YOUR_CONNECTOR": "YOUR_CONNECTOR", # Example
}
```
### Why This Matters
This mapping is used by `_map_connectors_to_searchable_types()` to:
1. Build the list of available search spaces for the LLM
2. Include connector types in the tool description
3. Enable the LLM to search your connector's data
**Without this mapping:**
- LLM won't know your connector exists
- LLM can't search your indexed data
- Users will get "I don't have access to that data" responses
### Verification
After adding the mapping, test with a user query:
```bash
# Example for DexScreener
curl -X POST http://localhost:8000/api/chat \
-H "Authorization: Bearer $TOKEN" \
-d '{
"message": "What is the current price of WETH?",
"space_id": 7
}'
```
**Expected:** LLM retrieves data and provides answer with citations.
**If failed:** Check that:
1. Connector is in `_CONNECTOR_TYPE_TO_SEARCHABLE`
2. Connector type matches exactly (case-sensitive)
3. Data is indexed in the correct `search_space_id`
---
## Best Practices
### 1. **Error Handling**

View file

@ -195,6 +195,11 @@ SurfSense cho phép bạn kết nối với **26+ ứng dụng bên ngoài** nh
- **Kết nối Google Drive:** Tìm files và documents ngay trong SurfSense
- **Kết nối Slack:** Tìm conversations và shared files từ workspace
- **Kết nối Notion:** Tìm kiếm trong pages và databases
- **Kết nối DexScreener:** Theo dõi giá crypto tokens real-time
- Không cần API key
- Chỉ cần nhập token addresses muốn theo dõi
- AI có thể trả lời: *"What's the current price of WETH?"*
- Xem trading pairs, liquidity, volume, price changes
**Quản lý Connectors:**

View file

@ -6,7 +6,7 @@
**Story Title**: DexScreener Connector Integration
**Epic**: SurfSense Connectors Enhancement
**Priority**: High
**Status**: Ready for Development
**Status**: ✅ Implementation Complete (2026-02-01)
**Created**: 2026-01-31
## 🎯 User Story
@ -27,63 +27,63 @@ This connector will integrate with SurfSense's existing connector architecture,
## ✅ Acceptance Criteria
### AC1: Connector Configuration
- [ ] User can add DexScreener connector via API endpoint
- [ ] User can configure multiple tokens to track (up to 50)
- [ ] Each token config includes: chain ID, token address, optional name
- [ ] User can update connector configuration
- [ ] User can delete connector
- [ ] Configuration is persisted in database
### AC1: Connector Configuration
- [x] User can add DexScreener connector via API endpoint
- [x] User can configure multiple tokens to track (up to 50)
- [x] Each token config includes: chain ID, token address, optional name
- [x] User can update connector configuration
- [x] User can delete connector
- [x] Configuration is persisted in database
### AC2: API Integration
- [ ] Connector successfully calls DexScreener API endpoints
- [ ] Handles rate limits (300 req/min) appropriately
- [ ] Implements retry logic with exponential backoff
- [ ] Validates API responses
- [ ] Handles API errors gracefully (network failures, invalid data, etc.)
### AC2: API Integration
- [x] Connector successfully calls DexScreener API endpoints
- [x] Handles rate limits (300 req/min) appropriately
- [x] Implements retry logic with exponential backoff
- [x] Validates API responses
- [x] Handles API errors gracefully (network failures, invalid data, etc.)
### AC3: Data Indexing
- [ ] Fetches trading pairs for all configured tokens
- [ ] Converts pair data to markdown format with all key metrics:
### AC3: Data Indexing
- [x] Fetches trading pairs for all configured tokens
- [x] Converts pair data to markdown format with all key metrics:
- Token information (names, symbols, addresses)
- Price data (USD, native, 24h changes)
- Volume metrics (24h, 6h, 1h)
- Liquidity information
- Market cap and FDV
- Transaction counts
- [ ] Generates unique identifier hash for each pair
- [ ] Generates content hash to detect changes
- [ ] Creates document chunks for vector search
- [ ] Generates embeddings using configured LLM
- [ ] Stores documents in database with proper metadata
- [ ] Updates existing documents when content changes
- [ ] Skips unchanged documents
- [x] Generates unique identifier hash for each pair
- [x] Generates content hash to detect changes
- [x] Creates document chunks for vector search
- [x] Generates embeddings using configured LLM
- [x] Stores documents in database with proper metadata
- [x] Updates existing documents when content changes
- [x] Skips unchanged documents
### AC4: Periodic Indexing
- [ ] Indexing task is registered with Celery
- [ ] Periodic scheduler triggers indexing (default: 60 min interval)
- [ ] Manual indexing can be triggered via API
- [ ] Last indexed timestamp is updated after successful indexing
- [ ] Indexing errors are logged properly
- [ ] Failed indexing doesn't block future attempts
### AC4: Periodic Indexing
- [x] Indexing task is registered with Celery
- [x] Periodic scheduler triggers indexing (default: 60 min interval)
- [x] Manual indexing can be triggered via API
- [x] Last indexed timestamp is updated after successful indexing
- [x] Indexing errors are logged properly
- [x] Failed indexing doesn't block future attempts
### AC5: Search Integration
- [ ] Indexed DexScreener data appears in search results
- [ ] Documents are searchable by:
### AC5: Search Integration
- [x] Indexed DexScreener data appears in search results
- [x] Documents are searchable by:
- Token names and symbols
- Pair addresses
- Chain IDs
- DEX names
- Price ranges
- Volume metrics
- [ ] Search results include relevant metadata
- [ ] Vector search returns semantically similar pairs
- [x] Search results include relevant metadata
- [x] Vector search returns semantically similar pairs
### AC6: AI Chat Integration
- [ ] AI chat can access DexScreener data as context
- [ ] Chat responses include relevant trading pair information
- [ ] Citations link to DexScreener URLs
- [ ] Metadata is properly formatted in chat responses
### AC6: AI Chat Integration
- [x] AI chat can access DexScreener data as context
- [x] Chat responses include relevant trading pair information
- [x] Citations link to DexScreener URLs
- [x] Metadata is properly formatted in chat responses
## 🏗️ Technical Implementation

View file

@ -6,7 +6,7 @@
**Story Title**: DexScreener Connector Frontend UI
**Epic**: SurfSense Connectors Enhancement
**Priority**: High
**Status**: Ready for Development
**Status**: ✅ Implementation Complete (2026-02-01)
**Created**: 2026-01-31
**Depends On**: Story 1.1 (Backend API)
@ -29,21 +29,21 @@ This story implements the user-facing components following SurfSense's establish
## ✅ Acceptance Criteria
### AC1: Connect Form Component
- [ ] User can access DexScreener connector from connector popup
- [ ] Form includes connector name field (min 3 characters)
- [ ] User can add multiple tokens (up to 50)
- [ ] Each token has: chain selector, token address input, optional name
- [ ] Form validates token addresses (40-character hex)
- [ ] User can remove tokens from list
- [ ] Date range selector for initial indexing
- [ ] Periodic sync toggle with frequency selector
- [ ] "What you get" benefits section displayed
- [ ] Form submits to backend API endpoint
### AC1: Connect Form Component
- [x] User can access DexScreener connector from connector popup
- [x] Form includes connector name field (min 3 characters)
- [x] User can add multiple tokens (up to 50)
- [x] Each token has: chain selector, token address input, optional name
- [x] Form validates token addresses (40-character hex)
- [x] User can remove tokens from list
- [x] Date range selector for initial indexing
- [x] Periodic sync toggle with frequency selector
- [x] "What you get" benefits section displayed
- [x] Form submits to backend API endpoint
### AC2: Token Management UI
- [ ] Dynamic token list with add/remove buttons
- [ ] Chain selector dropdown with popular chains:
### AC2: Token Management UI
- [x] Dynamic token list with add/remove buttons
- [x] Chain selector dropdown with popular chains:
- Ethereum
- BSC (Binance Smart Chain)
- Polygon
@ -52,42 +52,42 @@ This story implements the user-facing components following SurfSense's establish
- Base
- Avalanche
- Solana
- [ ] Token address input with validation
- [ ] Optional token name/label field
- [ ] Visual feedback for validation errors
- [ ] Responsive design for mobile/desktop
- [x] Token address input with validation
- [x] Optional token name/label field
- [x] Visual feedback for validation errors
- [x] Responsive design for mobile/desktop
### AC3: Connector Config Component
- [ ] Edit mode for existing connector
- [ ] Update connector name
- [ ] Add/remove tokens from tracked list
- [ ] View current token configuration
- [ ] Save changes button
- [ ] Cancel/discard changes option
### AC3: Connector Config Component
- [x] Edit mode for existing connector
- [x] Update connector name
- [x] Add/remove tokens from tracked list
- [x] View current token configuration
- [x] Save changes button
- [x] Cancel/discard changes option
### AC4: Connector Benefits
- [ ] Display benefits list in connect form
- [ ] Benefits include:
### AC4: Connector Benefits
- [x] Display benefits list in connect form
- [x] Benefits include:
- "Real-time cryptocurrency trading pair data"
- "Track prices, volume, and liquidity across multiple DEXs"
- "Search and analyze token market data with AI"
- "Monitor your crypto portfolio with automated updates"
- "Access historical price and volume trends"
### AC5: Documentation
- [ ] MDX documentation file created
- [ ] Setup guide with screenshots
- [ ] Token configuration instructions
- [ ] Chain selection guide
- [ ] Troubleshooting section
- [ ] Link to DexScreener API docs
### AC5: Documentation
- [x] MDX documentation file created
- [x] Setup guide with screenshots
- [x] Token configuration instructions
- [x] Chain selection guide
- [x] Troubleshooting section
- [x] Link to DexScreener API docs
### AC6: Integration
- [ ] Connector registered in connector registry
- [ ] Icon/logo added to public assets
- [ ] Connector appears in connector list
- [ ] Form properly integrated with connector popup
- [ ] Config component properly integrated
### AC6: Integration
- [x] Connector registered in connector registry
- [x] Icon/logo added to public assets
- [x] Connector appears in connector list
- [x] Form properly integrated with connector popup
- [x] Config component properly integrated
## 🏗️ Technical Implementation

View file

@ -0,0 +1,204 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DexScreenerConnectForm } from '@/components/assistant-ui/connector-popup/connect-forms/components/dexscreener-connect-form';
// Mock the form submission
const mockOnSubmit = vi.fn();
const mockOnBack = vi.fn();
describe('DexScreenerConnectForm', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Initial Rendering', () => {
it('should render the form with all required fields', () => {
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
// Check for connector name input
expect(screen.getByLabelText('Connector Name')).toBeInTheDocument();
// Check for benefits section
expect(screen.getByText('No API Key Required')).toBeInTheDocument();
// Check for add token button
expect(screen.getByRole('button', { name: /add token/i })).toBeInTheDocument();
});
it('should display the DexScreener info alert', () => {
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
expect(screen.getByText('No API Key Required')).toBeInTheDocument();
expect(screen.getByText(/DexScreener API is public and free to use/i)).toBeInTheDocument();
});
});
describe('Form Validation', () => {
it('should accept valid connector name', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
const nameInput = screen.getByLabelText('Connector Name');
await user.clear(nameInput);
await user.type(nameInput, 'Valid Name');
// Should accept valid name
expect(nameInput).toHaveValue('Valid Name');
});
it('should accept valid Ethereum address format', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
const addressInput = screen.getByLabelText('Token Address');
const validAddress = '0x' + 'a'.repeat(40);
await user.clear(addressInput);
await user.type(addressInput, validAddress);
// Should accept valid address
expect(addressInput).toHaveValue(validAddress);
});
});
describe('Token Management', () => {
it('should add a new token when Add Token button is clicked', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
// Initially should have 1 token (default)
expect(screen.getByText('Token #1')).toBeInTheDocument();
const addTokenButton = screen.getByRole('button', { name: /add token/i });
await user.click(addTokenButton);
// Should now have 2 tokens
await waitFor(() => {
expect(screen.getByText('Token #2')).toBeInTheDocument();
});
});
it('should remove a token when remove button is clicked', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
// Add a second token first
const addTokenButton = screen.getByRole('button', { name: /add token/i });
await user.click(addTokenButton);
await waitFor(() => {
expect(screen.getByText('Token #2')).toBeInTheDocument();
});
// Remove the second token
const removeButtons = screen.getAllByRole('button', { name: '' }); // X buttons have no text
const lastRemoveButton = removeButtons[removeButtons.length - 1];
await user.click(lastRemoveButton);
// Token #2 should be gone
await waitFor(() => {
expect(screen.queryByText('Token #2')).not.toBeInTheDocument();
});
});
it('should allow adding multiple tokens up to the limit', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
const addTokenButton = screen.getByRole('button', { name: /add token/i });
// Add 2 more tokens (already have 1)
await user.click(addTokenButton);
await user.click(addTokenButton);
// Should have 3 tokens total
await waitFor(() => {
expect(screen.getByText('Token #3')).toBeInTheDocument();
expect(screen.getByText('3 / 50 tokens')).toBeInTheDocument();
});
});
it('should disable Add Token button when maximum tokens (50) are reached', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
const addTokenButton = screen.getByRole('button', { name: /add token/i });
// Add 49 more tokens (already have 1) - this is slow but necessary
for (let i = 0; i < 49; i++) {
await user.click(addTokenButton);
}
// Button should be disabled and show max message
await waitFor(() => {
expect(addTokenButton).toBeDisabled();
expect(screen.getByText(/maximum reached/i)).toBeInTheDocument();
}, { timeout: 10000 });
});
});
describe('Chain Selection', () => {
it('should display supported chains in the dropdown', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
// Click on the chain selector
const chainSelect = screen.getByRole('combobox', { name: /chain/i });
await user.click(chainSelect);
// Check for supported chains
await waitFor(() => {
expect(screen.getByRole('option', { name: /ethereum/i })).toBeInTheDocument();
expect(screen.getByRole('option', { name: /bsc/i })).toBeInTheDocument();
expect(screen.getByRole('option', { name: /polygon/i })).toBeInTheDocument();
});
});
it('should allow chain selection', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
const chainSelect = screen.getByRole('combobox', { name: /chain/i });
await user.click(chainSelect);
// Select BSC
const bscOption = screen.getByRole('option', { name: /bsc/i });
await user.click(bscOption);
// Verify dropdown closed (option should not be visible anymore)
await waitFor(() => {
expect(screen.queryByRole('option', { name: /bsc/i })).not.toBeInTheDocument();
});
});
});
describe('Form Submission', () => {
it('should call onSubmit with valid data', async () => {
const user = userEvent.setup();
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
// Fill connector name
const nameInput = screen.getByLabelText('Connector Name');
await user.clear(nameInput);
await user.type(nameInput, 'My Connector');
// Fill token address (first token already exists)
const addressInput = screen.getByLabelText('Token Address');
const validAddress = '0x' + 'a'.repeat(40);
await user.type(addressInput, validAddress);
// Find and click submit button
const buttons = screen.getAllByRole('button');
const submitButton = buttons.find(btn => btn.textContent?.includes('Connect'));
if (submitButton) {
await user.click(submitButton);
// Verify onSubmit was called
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled();
}, { timeout: 3000 });
}
});
});
});

View file

@ -18,7 +18,10 @@
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"format:fix": "npx @biomejs/biome check --fix"
"format:fix": "npx @biomejs/biome check --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@ai-sdk/react": "^1.2.12",
@ -114,18 +117,25 @@
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^20.19.9",
"@types/pg": "^8.15.5",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.5",
"eslint": "^9.32.0",
"eslint-config-next": "15.2.0",
"jsdom": "^25.0.1",
"tailwindcss": "^4.1.11",
"tsx": "^4.20.6",
"typescript": "^5.8.3",
"vite": "^7.3.1"
"vite": "^7.3.1",
"vitest": "^3.2.4"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.ts',
css: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'vitest.setup.ts',
'**/*.config.ts',
'**/*.d.ts',
'**/types/**',
],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './'),
},
},
});

View file

@ -0,0 +1,43 @@
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
import React from 'react';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock ResizeObserver (required for Radix UI components)
global.ResizeObserver = class ResizeObserver {
observe() { }
unobserve() { }
disconnect() { }
};
// Mock PointerEvent APIs for Radix UI Select
HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
HTMLElement.prototype.setPointerCapture = vi.fn();
HTMLElement.prototype.releasePointerCapture = vi.fn();
HTMLElement.prototype.scrollIntoView = vi.fn();
// Mock Next.js router
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
pathname: '/',
query: {},
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams(),
}));
// Mock Next.js Image component
vi.mock('next/image', () => ({
default: (props: any) => {
return React.createElement('img', props);
},
}));