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ụ 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. - 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). 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 - 🎫 **Jira** - Tìm kiếm tickets
- 📚 **Confluence** - Tìm kiếm wiki pages - 📚 **Confluence** - Tìm kiếm wiki pages
- 🗂️ **Microsoft Teams** - Tìm kiếm chats và files - 🗂️ **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 - Chỉ hoạt động khi SurfSense chạy self-hosted
- Truy cập trực tiếp vào local file system - 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 ## 🛠️ 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 ## 💡 Use Cases
@ -320,6 +363,23 @@ Mỗi connector có các settings:
3. Slack discussion về issue 3. Slack discussion về issue
4. Confluence: Payment API Architecture 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 ## 🚨 Lưu Ý Quan Trọng

View file

@ -796,6 +796,76 @@ async def index_connector_data(connector: SearchSourceConnector):
# ... other connector types # ... 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 ## Best Practices
### 1. **Error Handling** ### 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 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 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 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:** **Quản lý Connectors:**

View file

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

View file

@ -6,7 +6,7 @@
**Story Title**: DexScreener Connector Frontend UI **Story Title**: DexScreener Connector Frontend UI
**Epic**: SurfSense Connectors Enhancement **Epic**: SurfSense Connectors Enhancement
**Priority**: High **Priority**: High
**Status**: Ready for Development **Status**: ✅ Implementation Complete (2026-02-01)
**Created**: 2026-01-31 **Created**: 2026-01-31
**Depends On**: Story 1.1 (Backend API) **Depends On**: Story 1.1 (Backend API)
@ -29,21 +29,21 @@ This story implements the user-facing components following SurfSense's establish
## ✅ Acceptance Criteria ## ✅ Acceptance Criteria
### AC1: Connect Form Component ### AC1: Connect Form Component
- [ ] User can access DexScreener connector from connector popup - [x] User can access DexScreener connector from connector popup
- [ ] Form includes connector name field (min 3 characters) - [x] Form includes connector name field (min 3 characters)
- [ ] User can add multiple tokens (up to 50) - [x] User can add multiple tokens (up to 50)
- [ ] Each token has: chain selector, token address input, optional name - [x] Each token has: chain selector, token address input, optional name
- [ ] Form validates token addresses (40-character hex) - [x] Form validates token addresses (40-character hex)
- [ ] User can remove tokens from list - [x] User can remove tokens from list
- [ ] Date range selector for initial indexing - [x] Date range selector for initial indexing
- [ ] Periodic sync toggle with frequency selector - [x] Periodic sync toggle with frequency selector
- [ ] "What you get" benefits section displayed - [x] "What you get" benefits section displayed
- [ ] Form submits to backend API endpoint - [x] Form submits to backend API endpoint
### AC2: Token Management UI ### AC2: Token Management UI
- [ ] Dynamic token list with add/remove buttons - [x] Dynamic token list with add/remove buttons
- [ ] Chain selector dropdown with popular chains: - [x] Chain selector dropdown with popular chains:
- Ethereum - Ethereum
- BSC (Binance Smart Chain) - BSC (Binance Smart Chain)
- Polygon - Polygon
@ -52,42 +52,42 @@ This story implements the user-facing components following SurfSense's establish
- Base - Base
- Avalanche - Avalanche
- Solana - Solana
- [ ] Token address input with validation - [x] Token address input with validation
- [ ] Optional token name/label field - [x] Optional token name/label field
- [ ] Visual feedback for validation errors - [x] Visual feedback for validation errors
- [ ] Responsive design for mobile/desktop - [x] Responsive design for mobile/desktop
### AC3: Connector Config Component ### AC3: Connector Config Component
- [ ] Edit mode for existing connector - [x] Edit mode for existing connector
- [ ] Update connector name - [x] Update connector name
- [ ] Add/remove tokens from tracked list - [x] Add/remove tokens from tracked list
- [ ] View current token configuration - [x] View current token configuration
- [ ] Save changes button - [x] Save changes button
- [ ] Cancel/discard changes option - [x] Cancel/discard changes option
### AC4: Connector Benefits ### AC4: Connector Benefits
- [ ] Display benefits list in connect form - [x] Display benefits list in connect form
- [ ] Benefits include: - [x] Benefits include:
- "Real-time cryptocurrency trading pair data" - "Real-time cryptocurrency trading pair data"
- "Track prices, volume, and liquidity across multiple DEXs" - "Track prices, volume, and liquidity across multiple DEXs"
- "Search and analyze token market data with AI" - "Search and analyze token market data with AI"
- "Monitor your crypto portfolio with automated updates" - "Monitor your crypto portfolio with automated updates"
- "Access historical price and volume trends" - "Access historical price and volume trends"
### AC5: Documentation ### AC5: Documentation
- [ ] MDX documentation file created - [x] MDX documentation file created
- [ ] Setup guide with screenshots - [x] Setup guide with screenshots
- [ ] Token configuration instructions - [x] Token configuration instructions
- [ ] Chain selection guide - [x] Chain selection guide
- [ ] Troubleshooting section - [x] Troubleshooting section
- [ ] Link to DexScreener API docs - [x] Link to DexScreener API docs
### AC6: Integration ### AC6: Integration
- [ ] Connector registered in connector registry - [x] Connector registered in connector registry
- [ ] Icon/logo added to public assets - [x] Icon/logo added to public assets
- [ ] Connector appears in connector list - [x] Connector appears in connector list
- [ ] Form properly integrated with connector popup - [x] Form properly integrated with connector popup
- [ ] Config component properly integrated - [x] Config component properly integrated
## 🏗️ Technical Implementation ## 🏗️ 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:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "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": { "dependencies": {
"@ai-sdk/react": "^1.2.12", "@ai-sdk/react": "^1.2.12",
@ -114,18 +117,25 @@
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16", "@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/canvas-confetti": "^1.9.0",
"@types/node": "^20.19.9", "@types/node": "^20.19.9",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"drizzle-kit": "^0.31.5", "drizzle-kit": "^0.31.5",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"eslint-config-next": "15.2.0", "eslint-config-next": "15.2.0",
"jsdom": "^25.0.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"tsx": "^4.20.6", "tsx": "^4.20.6",
"typescript": "^5.8.3", "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);
},
}));