test: Add Vitest and tests for Toast system

- Configure Vitest with jsdom environment
- Add test setup with @testing-library/jest-dom
- Add 6 tests for toastStore (addToast, removeToast, clearToasts)
- Add 8 tests for useToast hook (success, error, warning, info, dismiss)

All 14 tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-04 01:17:52 -06:00
parent 380b96e159
commit 434990972e
5 changed files with 264 additions and 2 deletions

View File

@ -9,7 +9,10 @@
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --ext ts,tsx --fix",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@hookform/resolvers": "^3.3.3",
@ -27,6 +30,9 @@
"zustand": "^4.4.7"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
@ -36,10 +42,12 @@
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^28.0.0",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.2.2",
"vite": "^5.0.8"
"vite": "^5.0.8",
"vitest": "^4.0.18"
},
"engines": {
"node": ">=18.0.0",

View File

@ -0,0 +1,134 @@
/**
* Tests for useToast hook
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useToast } from './useToast';
import { useToastStore } from '../stores/toastStore';
describe('useToast', () => {
beforeEach(() => {
// Reset store before each test
useToastStore.setState({ toasts: [] });
});
describe('success', () => {
it('should add a success toast', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.success('Operation successful');
});
const state = useToastStore.getState();
expect(state.toasts).toHaveLength(1);
expect(state.toasts[0].type).toBe('success');
expect(state.toasts[0].message).toBe('Operation successful');
});
it('should support custom title and duration', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.success('Saved', { title: 'Success!', duration: 3000 });
});
const state = useToastStore.getState();
expect(state.toasts[0].title).toBe('Success!');
expect(state.toasts[0].duration).toBe(3000);
});
});
describe('error', () => {
it('should add an error toast', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.error('Something went wrong');
});
const state = useToastStore.getState();
expect(state.toasts[0].type).toBe('error');
});
});
describe('warning', () => {
it('should add a warning toast', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.warning('Please check your input');
});
const state = useToastStore.getState();
expect(state.toasts[0].type).toBe('warning');
});
});
describe('info', () => {
it('should add an info toast', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.info('FYI: New feature available');
});
const state = useToastStore.getState();
expect(state.toasts[0].type).toBe('info');
});
});
describe('dismiss', () => {
it('should dismiss a specific toast', () => {
const { result } = renderHook(() => useToast());
let toastId: string;
act(() => {
toastId = result.current.success('Test');
});
expect(useToastStore.getState().toasts).toHaveLength(1);
act(() => {
result.current.dismiss(toastId);
});
expect(useToastStore.getState().toasts).toHaveLength(0);
});
});
describe('dismissAll', () => {
it('should dismiss all toasts', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.success('First');
result.current.error('Second');
result.current.warning('Third');
});
expect(useToastStore.getState().toasts).toHaveLength(3);
act(() => {
result.current.dismissAll();
});
expect(useToastStore.getState().toasts).toHaveLength(0);
});
});
describe('toast (generic)', () => {
it('should add toast with specified type', () => {
const { result } = renderHook(() => useToast());
act(() => {
result.current.toast('info', 'Generic toast');
});
const state = useToastStore.getState();
expect(state.toasts[0].type).toBe('info');
expect(state.toasts[0].message).toBe('Generic toast');
});
});
});

View File

@ -0,0 +1,102 @@
/**
* Tests for toastStore
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useToastStore } from './toastStore';
describe('toastStore', () => {
beforeEach(() => {
// Reset store before each test
useToastStore.setState({ toasts: [] });
});
describe('addToast', () => {
it('should add a toast with generated id', () => {
const { addToast } = useToastStore.getState();
const id = addToast({
type: 'success',
message: 'Test message',
});
const state = useToastStore.getState();
expect(state.toasts).toHaveLength(1);
expect(state.toasts[0]).toMatchObject({
id,
type: 'success',
message: 'Test message',
});
});
it('should add toast with optional title and duration', () => {
const { addToast } = useToastStore.getState();
addToast({
type: 'error',
message: 'Error message',
title: 'Error Title',
duration: 3000,
});
const state = useToastStore.getState();
expect(state.toasts[0]).toMatchObject({
type: 'error',
message: 'Error message',
title: 'Error Title',
duration: 3000,
});
});
it('should add multiple toasts', () => {
const { addToast } = useToastStore.getState();
addToast({ type: 'success', message: 'First' });
addToast({ type: 'error', message: 'Second' });
addToast({ type: 'warning', message: 'Third' });
const state = useToastStore.getState();
expect(state.toasts).toHaveLength(3);
});
});
describe('removeToast', () => {
it('should remove a toast by id', () => {
const { addToast, removeToast } = useToastStore.getState();
const id = addToast({ type: 'info', message: 'Test' });
expect(useToastStore.getState().toasts).toHaveLength(1);
removeToast(id);
expect(useToastStore.getState().toasts).toHaveLength(0);
});
it('should only remove specified toast', () => {
const { addToast, removeToast } = useToastStore.getState();
const id1 = addToast({ type: 'success', message: 'First' });
const id2 = addToast({ type: 'error', message: 'Second' });
removeToast(id1);
const state = useToastStore.getState();
expect(state.toasts).toHaveLength(1);
expect(state.toasts[0].id).toBe(id2);
});
});
describe('clearToasts', () => {
it('should remove all toasts', () => {
const { addToast, clearToasts } = useToastStore.getState();
addToast({ type: 'success', message: 'First' });
addToast({ type: 'error', message: 'Second' });
addToast({ type: 'warning', message: 'Third' });
expect(useToastStore.getState().toasts).toHaveLength(3);
clearToasts();
expect(useToastStore.getState().toasts).toHaveLength(0);
});
});
});

1
web/src/test/setup.ts Normal file
View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

17
web/vitest.config.ts Normal file
View File

@ -0,0 +1,17 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/'],
},
},
});