Testing Guide¶
Comprehensive testing strategies for Dash CopilotKit Components.
Testing Overview¶
Our testing strategy covers: - Unit tests for individual components - Integration tests for component interactions - End-to-end tests for complete workflows - Performance tests for optimization - Visual regression tests for UI consistency
Test Setup¶
JavaScript Testing¶
# Install testing dependencies
npm install --save-dev \
jest \
@testing-library/react \
@testing-library/jest-dom \
@testing-library/user-event \
jest-environment-jsdom
Python Testing¶
# Install Python testing dependencies
pip install pytest pytest-dash pytest-cov selenium webdriver-manager
Jest Configuration¶
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Test Setup File¶
// tests/setup.js
import '@testing-library/jest-dom';
// Mock CopilotKit components for testing
jest.mock('@copilotkit/react-ui', () => ({
CopilotChat: ({ children, ...props }) => <div data-testid="copilot-chat" {...props}>{children}</div>,
CopilotPopup: ({ children, ...props }) => <div data-testid="copilot-popup" {...props}>{children}</div>,
CopilotSidebar: ({ children, ...props }) => <div data-testid="copilot-sidebar" {...props}>{children}</div>,
CopilotTextarea: ({ children, ...props }) => <textarea data-testid="copilot-textarea" {...props}>{children}</textarea>
}));
jest.mock('@copilotkit/react-core', () => ({
CopilotKitProvider: ({ children }) => <div data-testid="copilotkit-provider">{children}</div>
}));
Unit Tests¶
Component Unit Tests¶
// tests/components/DashCopilotkitComponents.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import DashCopilotkitComponents from '../../src/lib/components/DashCopilotkitComponents.react';
describe('DashCopilotkitComponents', () => {
const defaultProps = {
id: 'test-component',
ui_type: 'chat',
public_api_key: 'test-key'
};
test('renders chat component by default', () => {
render(<DashCopilotkitComponents {...defaultProps} />);
expect(screen.getByTestId('copilot-chat')).toBeInTheDocument();
expect(screen.getByTestId('copilotkit-provider')).toBeInTheDocument();
});
test('renders different UI types', () => {
const { rerender } = render(
<DashCopilotkitComponents {...defaultProps} ui_type="popup" />
);
expect(screen.getByTestId('copilot-popup')).toBeInTheDocument();
rerender(<DashCopilotkitComponents {...defaultProps} ui_type="sidebar" />);
expect(screen.getByTestId('copilot-sidebar')).toBeInTheDocument();
rerender(<DashCopilotkitComponents {...defaultProps} ui_type="textarea" />);
expect(screen.getByTestId('copilot-textarea')).toBeInTheDocument();
});
test('handles prop changes', () => {
const { rerender } = render(
<DashCopilotkitComponents {...defaultProps} disabled={false} />
);
let component = screen.getByTestId('copilot-chat');
expect(component).not.toHaveAttribute('disabled');
rerender(<DashCopilotkitComponents {...defaultProps} disabled={true} />);
component = screen.getByTestId('copilot-chat');
expect(component).toHaveAttribute('disabled');
});
test('calls setProps on value change', async () => {
const mockSetProps = jest.fn();
const user = userEvent.setup();
render(
<DashCopilotkitComponents
{...defaultProps}
ui_type="textarea"
setProps={mockSetProps}
/>
);
const textarea = screen.getByTestId('copilot-textarea');
await user.type(textarea, 'Hello World');
await waitFor(() => {
expect(mockSetProps).toHaveBeenCalledWith({ value: 'Hello World' });
});
});
test('applies custom styling', () => {
const customStyle = { backgroundColor: 'red', width: '500px' };
const customClassName = 'custom-class';
render(
<DashCopilotkitComponents
{...defaultProps}
style={customStyle}
className={customClassName}
/>
);
const container = screen.getByTestId('copilot-chat').closest('div');
expect(container).toHaveClass(customClassName);
expect(container).toHaveStyle(customStyle);
});
test('validates prop types', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<DashCopilotkitComponents
{...defaultProps}
ui_type="invalid-type"
/>
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Warning: Failed prop type')
);
consoleSpy.mockRestore();
});
});
Hook Tests¶
// tests/hooks/useCopilotState.test.js
import { renderHook, act } from '@testing-library/react';
import { useCopilotState } from '../../src/lib/hooks/useCopilotState';
describe('useCopilotState', () => {
test('initializes with default value', () => {
const { result } = renderHook(() => useCopilotState('initial value'));
expect(result.current.value).toBe('initial value');
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
test('updates value and calls setProps', () => {
const mockSetProps = jest.fn();
const { result } = renderHook(() => useCopilotState('', mockSetProps));
act(() => {
result.current.setValue('new value');
});
expect(result.current.value).toBe('new value');
expect(mockSetProps).toHaveBeenCalledWith({ value: 'new value' });
});
test('handles errors correctly', () => {
const mockSetProps = jest.fn();
const { result } = renderHook(() => useCopilotState('', mockSetProps));
const testError = new Error('Test error');
act(() => {
result.current.handleError(testError);
});
expect(result.current.error).toBe(testError);
expect(mockSetProps).toHaveBeenCalledWith({ error: 'Test error' });
});
});
Integration Tests¶
Python Integration Tests¶
# tests/test_integration.py
import pytest
from dash import Dash, html, Input, Output, callback
import dash_copilotkit_components
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestDashIntegration:
@pytest.fixture
def app(self):
"""Create test Dash app"""
app = Dash(__name__)
app.layout = html.Div([
dash_copilotkit_components.DashCopilotkitComponents(
id='test-component',
ui_type='chat',
public_api_key='test-key',
instructions='You are a test assistant.'
),
html.Div(id='output')
])
@callback(
Output('output', 'children'),
Input('test-component', 'value')
)
def update_output(value):
return f"Value: {value}"
return app
def test_component_renders(self, app):
"""Test that component renders in Dash app"""
assert app.layout is not None
# Check component is in layout
component = None
for child in app.layout.children:
if hasattr(child, 'id') and child.id == 'test-component':
component = child
break
assert component is not None
assert component.ui_type == 'chat'
assert component.public_api_key == 'test-key'
def test_callback_interaction(self, app):
"""Test callback interactions"""
# This would require a more complex setup with dash testing utilities
pass
class TestSeleniumIntegration:
@pytest.fixture
def driver(self):
"""Setup Selenium WebDriver"""
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
@pytest.fixture
def dash_app(self):
"""Create and run Dash app for Selenium tests"""
app = Dash(__name__)
app.layout = html.Div([
dash_copilotkit_components.DashCopilotkitComponents(
id='selenium-test',
ui_type='textarea',
public_api_key='test-key',
placeholder='Type here...'
)
])
return app
def test_component_interaction(self, driver, dash_app):
"""Test component interaction with Selenium"""
# Start the app (this would need proper setup)
# driver.get('http://localhost:8050')
# Wait for component to load
# wait = WebDriverWait(driver, 10)
# textarea = wait.until(
# EC.presence_of_element_located((By.CSS_SELECTOR, '[data-testid="copilot-textarea"]'))
# )
# Test interaction
# textarea.send_keys('Hello World')
# assert textarea.get_attribute('value') == 'Hello World'
pass # Placeholder for actual implementation
Component Interaction Tests¶
# tests/test_component_interactions.py
import pytest
from dash import Dash, html, dcc, Input, Output, State, callback
import dash_copilotkit_components
def test_multiple_components():
"""Test multiple CopilotKit components in one app"""
app = Dash(__name__)
app.layout = html.Div([
dash_copilotkit_components.DashCopilotkitComponents(
id='chat-component',
ui_type='chat',
public_api_key='test-key'
),
dash_copilotkit_components.DashCopilotkitComponents(
id='textarea-component',
ui_type='textarea',
public_api_key='test-key'
),
html.Div(id='output')
])
@callback(
Output('output', 'children'),
[Input('chat-component', 'value'),
Input('textarea-component', 'value')]
)
def update_output(chat_value, textarea_value):
return f"Chat: {chat_value}, Textarea: {textarea_value}"
# Test that app initializes without errors
assert app.layout is not None
def test_dynamic_component_creation():
"""Test dynamically creating components"""
app = Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(
id='ui-type-dropdown',
options=[
{'label': 'Chat', 'value': 'chat'},
{'label': 'Popup', 'value': 'popup'},
{'label': 'Sidebar', 'value': 'sidebar'},
{'label': 'Textarea', 'value': 'textarea'}
],
value='chat'
),
html.Div(id='dynamic-component')
])
@callback(
Output('dynamic-component', 'children'),
Input('ui-type-dropdown', 'value')
)
def create_component(ui_type):
return dash_copilotkit_components.DashCopilotkitComponents(
id=f'dynamic-{ui_type}',
ui_type=ui_type,
public_api_key='test-key'
)
assert app.layout is not None
Performance Tests¶
Load Testing¶
# tests/test_performance.py
import time
import pytest
from dash import Dash, html
import dash_copilotkit_components
import psutil
import threading
class TestPerformance:
def test_component_creation_time(self):
"""Test component creation performance"""
start_time = time.time()
for i in range(100):
component = dash_copilotkit_components.DashCopilotkitComponents(
id=f'perf-test-{i}',
ui_type='chat',
public_api_key='test-key'
)
end_time = time.time()
creation_time = end_time - start_time
# Should create 100 components in less than 1 second
assert creation_time < 1.0
def test_memory_usage(self):
"""Test memory usage with multiple components"""
process = psutil.Process()
initial_memory = process.memory_info().rss
components = []
for i in range(50):
component = dash_copilotkit_components.DashCopilotkitComponents(
id=f'memory-test-{i}',
ui_type='chat',
public_api_key='test-key'
)
components.append(component)
final_memory = process.memory_info().rss
memory_increase = final_memory - initial_memory
# Memory increase should be reasonable (less than 50MB)
assert memory_increase < 50 * 1024 * 1024
def test_concurrent_component_usage(self):
"""Test concurrent component operations"""
def create_components():
for i in range(10):
component = dash_copilotkit_components.DashCopilotkitComponents(
id=f'concurrent-{threading.current_thread().ident}-{i}',
ui_type='chat',
public_api_key='test-key'
)
threads = []
start_time = time.time()
for i in range(5):
thread = threading.Thread(target=create_components)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
total_time = end_time - start_time
# Should complete in reasonable time
assert total_time < 5.0
Benchmark Tests¶
// tests/benchmark.test.js
import { performance } from 'perf_hooks';
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import DashCopilotkitComponents from '../src/lib/components/DashCopilotkitComponents.react';
describe('Performance Benchmarks', () => {
afterEach(cleanup);
test('component render time', () => {
const iterations = 100;
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
render(
<DashCopilotkitComponents
id={`benchmark-${i}`}
ui_type="chat"
public_api_key="test-key"
/>
);
const end = performance.now();
times.push(end - start);
cleanup();
}
const averageTime = times.reduce((a, b) => a + b, 0) / times.length;
const maxTime = Math.max(...times);
// Average render time should be less than 10ms
expect(averageTime).toBeLessThan(10);
// Max render time should be less than 50ms
expect(maxTime).toBeLessThan(50);
});
test('memory usage during renders', () => {
const initialMemory = process.memoryUsage().heapUsed;
// Render many components
for (let i = 0; i < 1000; i++) {
render(
<DashCopilotkitComponents
id={`memory-test-${i}`}
ui_type="chat"
public_api_key="test-key"
/>
);
cleanup();
}
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
// Memory increase should be minimal (less than 10MB)
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024);
});
});
Visual Regression Tests¶
Screenshot Testing¶
// tests/visual.test.js
import puppeteer from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
describe('Visual Regression Tests', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800 });
});
afterAll(async () => {
await browser.close();
});
test('chat component visual', async () => {
// Navigate to test page with chat component
await page.goto('http://localhost:8050/test-chat');
// Wait for component to load
await page.waitForSelector('[data-testid="copilot-chat"]');
// Take screenshot
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot({
threshold: 0.2,
thresholdType: 'percent'
});
});
test('popup component visual', async () => {
await page.goto('http://localhost:8050/test-popup');
await page.waitForSelector('[data-testid="copilot-popup"]');
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot();
});
});
Test Automation¶
GitHub Actions¶
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
npm ci
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run JavaScript tests
run: npm test -- --coverage --watchAll=false
- name: Run Python tests
run: pytest tests/ --cov=dash_copilotkit_components --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info,./coverage.xml
Test Scripts¶
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --coverage --watchAll=false",
"test:visual": "jest --testPathPattern=visual",
"test:performance": "jest --testPathPattern=performance"
}
}
Test Best Practices¶
Writing Good Tests¶
- Test behavior, not implementation
- Use descriptive test names
- Keep tests isolated and independent
- Mock external dependencies
- Test edge cases and error conditions
Test Organization¶
tests/
├── components/ # Component unit tests
├── hooks/ # Hook tests
├── integration/ # Integration tests
├── performance/ # Performance tests
├── visual/ # Visual regression tests
├── fixtures/ # Test data and fixtures
├── utils/ # Test utilities
└── setup.js # Test setup
Next Steps¶
- Development Guide - Development workflow
- Building Guide - Build process
- Deployment - Deploy tested components