frontend-testing
Unit and component testing for the frontend with Vitest and Testing Library. Keywords: Vitest, @testing-library/svelte, component tests, vi.mock, render, screen, fireEvent, userEvent, test.ts, spec.ts, describe, it, AAA pattern
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Frontend Testing
Documentation: vitest.dev | testing-library.com
Running Tests
npm run test:unit
Framework & Location
- Framework: Vitest + @testing-library/svelte
-
Location: Co-locate with code as
.test.tsor.spec.ts - TDD workflow: When fixing bugs or adding features, write a failing test first
AAA Pattern
Use explicit Arrange, Act, Assert regions:
import { describe, expect, it } from 'vitest';
describe('Calculator', () => {
it('should add two numbers correctly', () => {
// Arrange
const a = 5;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(8);
});
it('should handle negative numbers', () => {
// Arrange
const a = -5;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(-2);
});
});
Test Patterns from Codebase
Unit Tests with AAA
From dates.test.ts:
import { describe, expect, it } from 'vitest';
import { getDifferenceInSeconds, getRelativeTimeFormatUnit } from './dates';
describe('getDifferenceInSeconds', () => {
it('should calculate difference in seconds correctly', () => {
// Arrange
const now = new Date();
const past = new Date(now.getTime() - 5000);
// Act
const result = getDifferenceInSeconds(past);
// Assert
expect(result).toBeCloseTo(5, 0);
});
});
describe('getRelativeTimeFormatUnit', () => {
it('should return correct unit for given seconds', () => {
// Arrange & Act & Assert (simple value tests)
expect(getRelativeTimeFormatUnit(30)).toBe('seconds');
expect(getRelativeTimeFormatUnit(1800)).toBe('minutes');
expect(getRelativeTimeFormatUnit(7200)).toBe('hours');
});
it('should handle boundary cases correctly', () => {
// Arrange & Act & Assert
expect(getRelativeTimeFormatUnit(59)).toBe('seconds');
expect(getRelativeTimeFormatUnit(60)).toBe('minutes');
});
});
Testing with Spies
From cached-persisted-state.svelte.test.ts:
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CachedPersistedState } from './cached-persisted-state.svelte';
describe('CachedPersistedState', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with default value when storage is empty', () => {
// Arrange & Act
const state = new CachedPersistedState('test-key', 'default');
// Assert
expect(state.current).toBe('default');
});
it('should return cached value without reading storage repeatedly', () => {
// Arrange
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem');
localStorage.setItem('test-key', 'value1');
const state = new CachedPersistedState('test-key', 'default');
getItemSpy.mockClear();
// Act
const val1 = state.current;
const val2 = state.current;
// Assert
expect(val1).toBe('value1');
expect(val2).toBe('value1');
expect(getItemSpy).not.toHaveBeenCalled();
});
});
Testing String Transformations
From helpers.svelte.test.ts:
import { describe, expect, it } from 'vitest';
import { quoteIfSpecialCharacters } from './helpers.svelte';
describe('helpers.svelte', () => {
it('quoteIfSpecialCharacters handles tabs and newlines', () => {
// Arrange & Act & Assert
expect(quoteIfSpecialCharacters('foo\tbar')).toBe('"foo\tbar"');
expect(quoteIfSpecialCharacters('foo\nbar')).toBe('"foo\nbar"');
});
it('quoteIfSpecialCharacters handles empty string and undefined/null', () => {
// Arrange & Act & Assert
expect(quoteIfSpecialCharacters('')).toBe('');
expect(quoteIfSpecialCharacters(undefined)).toBeUndefined();
expect(quoteIfSpecialCharacters(null)).toBeNull();
});
it('quoteIfSpecialCharacters quotes all Lucene special characters', () => {
// Arrange
const luceneSpecials = ['+', '-', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\', '/'];
// Act & Assert
for (const char of luceneSpecials) {
expect(quoteIfSpecialCharacters(char)).toBe(`"${char}"`);
}
});
});
Query Selection Priority
Use accessible queries (not implementation details):
// ✅ Role-based
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
// ✅ Label-based
screen.getByLabelText('Email address');
// ✅ Text-based
screen.getByText('Welcome back');
// ⚠️ Fallback: Test ID
screen.getByTestId('complex-chart');
// ❌ Avoid: Implementation details
screen.getByClassName('btn-primary');
Mocking Modules
import { vi, describe, it, beforeEach, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
vi.mock('$lib/api/organizations', () => ({
getOrganizations: vi.fn()
}));
import { getOrganizations } from '$lib/api/organizations';
import OrganizationList from './organization-list.svelte';
describe('OrganizationList', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('displays organizations from API', async () => {
// Arrange
const mockOrganizations = [{ id: '1', name: 'Org One' }];
vi.mocked(getOrganizations).mockResolvedValue(mockOrganizations);
// Act
render(OrganizationList);
// Assert
expect(await screen.findByText('Org One')).toBeInTheDocument();
});
});
Snapshot Testing (Use Sparingly)
it('matches snapshot', () => {
// Arrange & Act
const { container } = render(StaticComponent);
// Assert
expect(container).toMatchSnapshot();
});
Use snapshots only for stable, static components. Prefer explicit assertions for dynamic content.
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-30
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!