mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-14 20:55:15 +02:00
test: Add Vitest configuration and initial tests for the DexScreener connect form.
This commit is contained in:
parent
828d7d695a
commit
fd9eddf7fa
11 changed files with 1480 additions and 116 deletions
|
|
@ -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**
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
929
surfsense_web/pnpm-lock.yaml
generated
929
surfsense_web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
29
surfsense_web/vitest.config.ts
Normal file
29
surfsense_web/vitest.config.ts
Normal 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, './'),
|
||||
},
|
||||
},
|
||||
});
|
||||
43
surfsense_web/vitest.setup.ts
Normal file
43
surfsense_web/vitest.setup.ts
Normal 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);
|
||||
},
|
||||
}));
|
||||
Loading…
Add table
Add a link
Reference in a new issue