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

@ -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);
},
}));