Timeline Studio uses a comprehensive testing strategy covering unit tests, integration tests, and end-to-end tests for both frontend and backend code.
- Frontend: Vitest + Testing Library
- Backend: Rust built-in testing + cargo-test
- E2E: Playwright (planned)
- Coverage: c8 for frontend, cargo-tarpaulin for backend
# Run all tests (frontend + backend)
bun run test:all
# Run with coverage
bun run test:coverage# Run all frontend tests
bun run test
# Run in watch mode
bun run test:watch
# Run specific test file
bun run test src/features/timeline/__tests__/use-timeline.test.ts
# Run with coverage
bun run test:coverage# Run all Rust tests
bun run test:rust
# Run specific module tests
cd src-tauri && cargo test recognition
# Run with verbose output
cd src-tauri && cargo test -- --nocapture# Run Playwright tests
bun run test:e2e
# Run in headed mode
bun run test:e2e --headedimport { render, screen } from '@/test/test-utils';
import { VideoPlayer } from '../components/video-player';
describe('VideoPlayer', () => {
it('should render play button', () => {
render(<VideoPlayer src="video.mp4" />);
expect(screen.getByRole('button', { name: /play/i })).toBeInTheDocument();
});
});import { renderHook, act } from '@testing-library/react';
import { useTimeline } from '../hooks/use-timeline';
describe('useTimeline', () => {
it('should add clip to timeline', () => {
const { result } = renderHook(() => useTimeline());
act(() => {
result.current.addClip({
id: '1',
source: 'video.mp4',
duration: 10
});
});
expect(result.current.clips).toHaveLength(1);
});
});import { createActor } from 'xstate';
import { timelineMachine } from '../services/timeline-machine';
describe('timelineMachine', () => {
it('should transition to playing state', async () => {
const actor = createActor(timelineMachine);
actor.start();
actor.send({ type: 'PLAY' });
expect(actor.getSnapshot().matches('playing')).toBe(true);
});
});#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_video_metadata_extraction() {
let metadata = extract_metadata("test.mp4").unwrap();
assert_eq!(metadata.duration, 120.0);
assert_eq!(metadata.width, 1920);
assert_eq!(metadata.height, 1080);
}
#[tokio::test]
async fn test_async_operation() {
let result = process_video("test.mp4").await;
assert!(result.is_ok());
}
}feature/
├── __tests__/
│ ├── components/
│ │ └── component.test.tsx
│ ├── hooks/
│ │ └── use-hook.test.ts
│ ├── services/
│ │ └── service.test.ts
│ └── utils/
│ └── util.test.ts
└── __mocks__/
└── service.mock.ts
module/
├── mod.rs
├── tests.rs # Unit tests
└── tests/
└── integration/ # Integration tests
// __mocks__/@tauri-apps/api/tauri.ts
export const invoke = vi.fn().mockImplementation((cmd: string, args?: any) => {
switch (cmd) {
case 'get_media_metadata':
return Promise.resolve({
duration: 120,
width: 1920,
height: 1080
});
default:
return Promise.resolve();
}
});// __mocks__/media-service.ts
export const MediaService = {
loadMedia: vi.fn().mockResolvedValue({
id: '1',
path: 'video.mp4',
type: 'video'
}),
getThumbnail: vi.fn().mockResolvedValue('data:image/png;base64,...')
};#[cfg(test)]
mod mock_ffmpeg {
pub fn extract_frame(_: &str, _: f64) -> Result<Vec<u8>> {
Ok(vec![0; 100]) // Mock image data
}
}// test/test-utils.tsx
import { render as rtlRender } from '@testing-library/react';
import { TimelineProvider } from '@/features/timeline/providers';
export function render(ui: React.ReactElement, options?: any) {
return rtlRender(ui, {
wrapper: ({ children }) => (
<TimelineProvider>
{children}
</TimelineProvider>
),
...options
});
}// test/builders/timeline.builder.ts
export class TimelineBuilder {
private timeline = {
tracks: [],
duration: 0
};
withTrack(track: Track) {
this.timeline.tracks.push(track);
return this;
}
build() {
return this.timeline;
}
}- Overall: >80%
- Critical modules: >90%
- Timeline
- Video Compiler
- Export
- New code: 100%
Module Coverage Target Status
─────────────────────────────────────────────────
timeline 84.44% 90% ⚠️
video-player 86.15% 80% ✅
export 3.64% 90% ❌
subtitles 8.41% 80% ❌
templates 9.63% 80% ❌
effects 84.61% 80% ✅
filters 81.48% 80% ✅
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
run: |
npm install -g bun
bun install
- name: Lint
run: bun run lint
- name: Test
run: bun run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3// Good
it('should display error message when video fails to load', () => {});
// Bad
it('test error', () => {});it('should update timeline when clip is moved', () => {
// Arrange
const timeline = createTimeline();
const clip = createClip();
// Act
timeline.moveClip(clip.id, 10);
// Assert
expect(timeline.getClip(clip.id).position).toBe(10);
});- Each test should be independent
- Clean up after tests
- Don't rely on test order
it('should load media file', async () => {
const media = await MediaService.load('video.mp4');
expect(media).toBeDefined();
expect(media.type).toBe('video');
});# Run single test with debugging
bun run test:debug path/to/test.ts
# Use TauriLogger in tests
import { createLogger } from '@/lib/tauri-logger'
const logger = createLogger('TestModule')
logger.debugSync('Test debug message')
DEBUG=* bun run test# Run with print output
cargo test -- --nocapture
# Run specific test
cargo test test_name -- --exactimport { bench } from 'vitest';
bench('timeline render performance', () => {
const timeline = createLargeTimeline(1000); // 1000 clips
renderTimeline(timeline);
});#[bench]
fn bench_video_processing(b: &mut Bencher) {
b.iter(|| {
process_video("large_video.mp4")
});
}it('should handle network errors gracefully', async () => {
mockInvoke.mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useMediaLoader());
await waitFor(() => {
expect(result.current.error).toBe('Failed to load media');
});
});it('should show loading spinner while fetching', async () => {
const { getByTestId, queryByTestId } = render(<MediaBrowser />);
expect(getByTestId('loading-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(queryByTestId('loading-spinner')).not.toBeInTheDocument();
});
});it('should delete clip when delete button clicked', async () => {
const user = userEvent.setup();
const onDelete = vi.fn();
render(<Clip onDelete={onDelete} />);
await user.click(screen.getByRole('button', { name: /delete/i }));
expect(onDelete).toHaveBeenCalledOnce();
});For specific testing examples, see the test files in each feature directory.