mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +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
|
|
@ -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