Tests JavaScript and TypeScript applications with Vitest including unit tests, mocking, coverage, and React component testing. Use when writing tests, setting up test infrastructure, mocking dependencies, or measuring code coverage.
Runs Vitest tests for JavaScript/TypeScript apps including unit tests, React components, mocking, and coverage. Use when writing tests, setting up test infrastructure, mocking dependencies, or measuring code coverage.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Blazing fast unit test framework powered by Vite with native TypeScript and ESM support.
Install:
npm install -D vitest
Add to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest --coverage"
}
}
Configure vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
resolve: {
alias: {
'@': '/src',
},
},
});
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
afterEach(() => {
// Cleanup
});
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
describe('division', () => {
it('should divide two numbers', () => {
expect(calculator.divide(6, 2)).toBe(3);
});
it('should throw on division by zero', () => {
expect(() => calculator.divide(6, 0)).toThrow('Division by zero');
});
});
});
import { expect } from 'vitest';
// Equality
expect(value).toBe(5); // Strict equality
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).toStrictEqual({ a: 1 }); // Strict deep equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3, 5); // Floating point
// Strings
expect(value).toMatch(/pattern/);
expect(value).toContain('substring');
// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));
// Objects
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', 'value');
expect(object).toMatchObject({ partial: true });
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('error message');
expect(() => fn()).toThrowError(/pattern/);
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow('error');
// Negation
expect(value).not.toBe(5);
import { describe, it, expect, vi } from 'vitest';
describe('async operations', () => {
// Async/await
it('should fetch data', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
// Returning promise
it('should resolve correctly', () => {
return expect(fetchData()).resolves.toEqual({ id: 1 });
});
// Using done callback
it('should call callback', (done) => {
fetchWithCallback((data) => {
expect(data).toBeDefined();
done();
});
});
// Testing rejected promises
it('should reject on error', async () => {
await expect(fetchInvalidData()).rejects.toThrow('Not found');
});
});
import { vi, describe, it, expect, beforeEach } from 'vitest';
describe('mocking functions', () => {
const mockFn = vi.fn();
beforeEach(() => {
mockFn.mockClear(); // Clear calls, keep implementation
// mockFn.mockReset(); // Clear everything
// mockFn.mockRestore(); // Restore original (for spies)
});
it('should track calls', () => {
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
it('should return mocked values', () => {
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
});
it('should mock implementation', () => {
mockFn.mockImplementation((x) => x * 2);
expect(mockFn(5)).toBe(10);
});
it('should mock resolved values', async () => {
mockFn.mockResolvedValue({ data: 'test' });
await expect(mockFn()).resolves.toEqual({ data: 'test' });
});
});
import { vi, describe, it, expect } from 'vitest';
// Mock entire module
vi.mock('./database', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
saveUser: vi.fn().mockResolvedValue(true),
}));
// Mock with factory
vi.mock('./api', () => {
return {
fetchPosts: vi.fn(() => Promise.resolve([])),
};
});
// Partial mock
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
formatDate: vi.fn(() => '2024-01-01'),
};
});
import { getUser } from './database';
import { fetchPosts } from './api';
describe('module mocking', () => {
it('should use mocked module', async () => {
const user = await getUser(1);
expect(user).toEqual({ id: 1, name: 'Test' });
});
});
import { vi, describe, it, expect } from 'vitest';
describe('spying', () => {
it('should spy on object method', () => {
const obj = {
method: (x: number) => x * 2,
};
const spy = vi.spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveReturnedWith(10);
spy.mockRestore();
});
it('should spy and mock', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
console.log('test');
expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();
});
});
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('timer mocking', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should advance timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
it('should run all timers', () => {
const callback = vi.fn();
setTimeout(callback, 100);
setTimeout(callback, 200);
vi.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
it('should mock Date', () => {
vi.setSystemTime(new Date(2024, 0, 1));
expect(new Date().getFullYear()).toBe(2024);
});
});
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('should render initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
it('should increment on click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should call onChange when count changes', async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<Counter initialCount={0} onChange={onChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(onChange).toHaveBeenCalledWith(1);
});
});
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should initialize with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should update when props change', () => {
const { result, rerender } = renderHook(
({ initial }) => useCounter(initial),
{ initialProps: { initial: 0 } }
);
expect(result.current.count).toBe(0);
rerender({ initial: 10 });
// Note: depends on hook implementation
});
});
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ThemeProvider } from './ThemeContext';
import { ThemedButton } from './ThemedButton';
const renderWithTheme = (ui: React.ReactElement, theme = 'light') => {
return render(
<ThemeProvider initialTheme={theme}>{ui}</ThemeProvider>
);
};
describe('ThemedButton', () => {
it('should apply light theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'light');
expect(screen.getByRole('button')).toHaveClass('theme-light');
});
it('should apply dark theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'dark');
expect(screen.getByRole('button')).toHaveClass('theme-dark');
});
});
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserProfile } from './UserProfile';
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'John Doe' }),
}));
describe('UserProfile', () => {
it('should show loading state', () => {
render(<UserProfile userId="1" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should display user after loading', async () => {
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
it('should show error state', async () => {
const { fetchUser } = await import('./api');
vi.mocked(fetchUser).mockRejectedValueOnce(new Error('Failed'));
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('Error loading user')).toBeInTheDocument();
});
});
});
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Card } from './Card';
describe('Card', () => {
it('should match snapshot', () => {
const { container } = render(
<Card title="Test" description="Description" />
);
expect(container).toMatchSnapshot();
});
it('should match inline snapshot', () => {
const { container } = render(<Card title="Test" />);
expect(container.innerHTML).toMatchInlineSnapshot(`
"<div class=\\"card\\"><h2>Test</h2></div>"
`);
});
});
Install coverage provider:
npm install -D @vitest/coverage-v8
Configure:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Run:
npm run test:coverage
// test/utils.tsx
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function renderWithProviders(
ui: React.ReactElement,
options = {}
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
options
);
}
export * from '@testing-library/react';
// test/factories.ts
interface User {
id: string;
name: string;
email: string;
}
export function createUser(overrides: Partial<User> = {}): User {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
...overrides,
};
}
// Usage
const user = createUser({ name: 'Custom Name' });
| Mistake | Fix |
|---|---|
| Testing implementation | Test behavior and outcomes |
| Over-mocking | Only mock external dependencies |
| Brittle selectors | Use accessible queries |
| Missing cleanup | Use afterEach cleanup |
| Ignoring async | Always await async operations |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.