Skill: Writing TypeScript Unit Tests
Overview
This skill covers writing unit tests for the frontend using Vitest.
Project Structure
frontend/ ├── package.json # vitest + jsdom devDependencies ├── vite.config.ts # Vitest config inline └── src/ ├── config/ │ └── tests/ │ └── authConfig.test.ts ├── utils/ │ ├── citationParser.ts │ ├── sseParser.ts │ ├── fileAttachments.ts │ └── tests/ │ ├── citationParser.test.ts │ ├── sseParser.test.ts │ └── fileAttachments.test.ts ├── services/ │ └── tests/ │ └── chatService.test.ts └── reducers/ ├── appReducer.ts └── tests/ └── appReducer.test.ts # Includes state shape snapshot
Configuration
The Vitest config lives inline in vite.config.ts :
export default defineConfig({ // ... existing config test: { globals: true, environment: "jsdom", include: ["src/**/*.test.{ts,tsx}"], }, });
Test Anatomy
import { describe, it, expect } from "vitest"; import { parseCitations, deduplicateAnnotations } from "../citationParser";
describe("citationParser", () => { describe("parseCitations", () => { it("returns empty array for empty input", () => { expect(parseCitations("", [])).toEqual([]); });
it("extracts citation markers from text", () => {
const text = "Hello [1] world [2]";
const result = parseCitations(text, annotations);
expect(result).toHaveLength(2);
});
});
describe("deduplicateAnnotations", () => { it("removes duplicate annotations by URL", () => { const annotations = [ { url: "https://example.com", title: "Example" }, { url: "https://example.com", title: "Example Duplicate" }, ]; expect(deduplicateAnnotations(annotations)).toHaveLength(1); }); }); });
Running Tests
Run tests in watch mode (interactive)
cd frontend npm test
Run tests once (CI mode)
npm run test:run
Run with coverage
npm run test:coverage
Run specific file
npx vitest run src/utils/tests/citationParser.test.ts
Run tests matching pattern
npx vitest run --testNamePattern="parseCitations"
Testable Units in This Project
Utils (Pure Functions - Easy to Test)
File Functions to Test
citationParser.ts
parseContentWithCitations
sseParser.ts
parseSseLine , splitSseBuffer
fileAttachments.ts
validateFile , validateImageFile , validateDocumentFile , validateFileCount , getEffectiveMimeType , convertFilesToDataUris
errorHandler.ts
getUserFriendlyMessage , createAppError , getErrorCodeFromResponse , parseErrorFromResponse , getErrorCodeFromMessage , isTokenExpiredError , isNetworkError , retryWithBackoff
Reducers (Pure Functions - Easy to Test)
File What to Test
appReducer.ts
All action types, state transitions, immutability
Components (Require React Testing Library)
For component testing, add @testing-library/react only when needed:
import { render, screen } from "@testing-library/react"; import { ChatMessage } from "../ChatMessage";
it("renders message content", () => { render(<ChatMessage role="user" content="Hello" />); expect(screen.getByText("Hello")).toBeInTheDocument(); });
Assertions Reference
Vitest uses Chai-style assertions via expect :
// Equality expect(actual).toBe(expected); // strict equality (===) expect(actual).toEqual(expected); // deep equality
// Truthiness expect(value).toBeTruthy(); expect(value).toBeFalsy(); expect(value).toBeNull(); expect(value).toBeUndefined();
// Numbers expect(num).toBeGreaterThan(5); expect(num).toBeLessThanOrEqual(10);
// Strings expect(str).toContain("substring"); expect(str).toMatch(/regex/);
// Arrays expect(arr).toHaveLength(3); expect(arr).toContain(item);
// Objects expect(obj).toHaveProperty("key"); expect(obj).toMatchObject({ partial: "match" });
// Exceptions expect(() => throwingFn()).toThrow(); expect(() => throwingFn()).toThrowError("message");
// Async await expect(asyncFn()).resolves.toBe(value); await expect(asyncFn()).rejects.toThrow();
Test Organization
Use describe blocks to group related tests:
describe("moduleName", () => { describe("functionName", () => { it("handles normal case", () => {}); it("handles edge case", () => {}); it("throws on invalid input", () => {}); }); });
State Shape Snapshot Tests
Use state shape snapshots to prevent accidental state changes from going unnoticed. If a new field is added to AppState without updating the test, it fails:
it('should have expected state shape (update this test when adding new state fields)', () => { const shape = JSON.stringify(Object.keys(initialAppState).sort()); expect(shape).toBe('["auth","chat","conversations","ui"]'); const convShape = JSON.stringify(Object.keys(initialAppState.conversations).sort()); expect(convShape).toBe('["hasMore","isLoading","list","sidebarOpen"]'); });
This forces anyone adding state fields to also add test coverage — the test file becomes the registry of all state. Apply this pattern to any new top-level state domain.
When Unit Tests Aren't Enough
Use the validating-ui-features skill and Playwright when:
-
Testing requires browser interaction (clicking, navigation)
-
Testing authentication flows with MSAL
-
Testing SSE streaming with real backend
-
Visual regression testing
Quick Reference
Command Purpose
npm test
Watch mode
npm run test:run
Run once
npm run test:coverage
With coverage
npx vitest --ui
Interactive UI