Adds Vitest and Playwright testing setup with sample tests
Some checks failed
Lint / Lint and Typecheck (push) Failing after 27s

Introduces a unified testing setup using Vitest for unit tests
and Playwright for E2E tests. Updates dependencies, adds sample
unit and E2E tests, documents test workflow, and codifies
testing and code standards in project guidelines.

Enables fast, automated test runs and improves code reliability
through enforced standards.
This commit is contained in:
2025-11-24 22:44:04 +01:00
parent 9666193c9f
commit 3d1d010100
10 changed files with 1653 additions and 8 deletions

33
.cursorrules Normal file
View File

@@ -0,0 +1,33 @@
# Cursor Rules for InvestingFIRE 🔥
## General Principles
- **Quality First:** All new features must include appropriate tests.
- **User-Centric:** Prioritize user experience and accessibility in all changes.
- **Dry Code:** Avoid duplication; use utility functions and components.
## Testing Requirements 🧪
- **Unit Tests:** Required for all new utility functions, hooks, and complex logic.
- Use `vitest` and `react-testing-library`.
- Place tests in `__tests__` directories or alongside files with `.test.ts(x)` extension.
- **E2E Tests:** Required for new user flows and critical paths.
- Use `playwright`.
- Ensure tests cover happy paths and error states.
- **Visual Regression:** Consider for major UI changes.
## Coding Standards
- **Type Safety:** No `any`. Use proper Zod schemas for validation.
- **Components:** Use functional components with strict prop typing.
- **Styling:** Use Tailwind CSS. Avoid inline styles.
- **State Management:** Prefer local state or React Context. Avoid global state libraries unless necessary.
## Workflow
1. **Plan:** Break down tasks.
2. **Implement:** Write clean, commented code.
3. **Test:** specific unit and/or E2E tests.
4. **Verify:** Run linter and type checker (`pnpm check`).
## Specific Patterns
- **Forms:** Use `react-hook-form` with `zod` resolvers.
- **Charts:** Use `recharts` and ensure tooltips are accessible.
- **Calculations:** Keep financial logic separate from UI components where possible (e.g., in `lib/` or custom hooks) to facilitate testing.

5
.gitignore vendored
View File

@@ -43,4 +43,7 @@ yarn-error.log*
*.tsbuildinfo
# idea files
.idea
.idea
playwright-report/
test-results/

View File

@@ -104,6 +104,24 @@ To run locally:
```
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.
### Running Tests 🧪
We use **Vitest** for unit testing and **Playwright** for end-to-end (E2E) testing.
**Unit Tests:**
```bash
pnpm test
```
**E2E Tests:**
```bash
# First install browsers (only needed once)
pnpm exec playwright install
# Run tests
pnpm test:e2e
```
---
## ✏️ Inputs & Variables

15
e2e/home.spec.ts Normal file
View File

@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
test("homepage has title and calculator", async ({ page }) => {
await page.goto("/");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/InvestingFIRE/);
// Check for main heading
await expect(page.getByRole("heading", { name: "InvestingFIRE" })).toBeVisible();
// Check for Calculator
await expect(page.getByText("FIRE Calculator")).toBeVisible();
});

View File

@@ -11,7 +11,9 @@
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"lint": "next typegen && eslint . && npx tsc --noEmit",
"preview": "next build && next start",
"start": "next start"
"start": "next start",
"test": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
@@ -34,20 +36,28 @@
"zod": "^4.0.0"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "4.1.17",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "24.10.1",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "9.39.1",
"eslint-config-next": "16.0.3",
"eslint-config-prettier": "^10.1.8",
"jsdom": "^27.2.0",
"postcss": "8.5.6",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.7.1",
"tailwindcss": "4.1.17",
"tw-animate-css": "1.4.0",
"typescript": "5.9.3",
"typescript-eslint": "8.47.0"
"typescript-eslint": "8.47.0",
"vitest": "^4.0.13"
},
"ct3aMetadata": {
"initVersion": "7.39.3"

34
playwright.config.ts Normal file
View File

@@ -0,0 +1,34 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "list",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
],
webServer: {
command: "pnpm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});

1496
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
import { render, screen } from "@testing-library/react";
import FireCalculatorForm from "../FireCalculatorForm";
import { describe, it, expect, vi } from "vitest";
// Mocking ResizeObserver because it's not available in jsdom and Recharts uses it
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = ResizeObserver;
describe("FireCalculatorForm", () => {
it("renders the form with default values", () => {
render(<FireCalculatorForm />);
expect(screen.getByText("FIRE Calculator")).toBeInTheDocument();
expect(screen.getByLabelText(/Starting Capital/i)).toHaveValue(50000);
expect(screen.getByLabelText(/Monthly Savings/i)).toHaveValue(1500);
});
it("renders the Calculate button", () => {
render(<FireCalculatorForm />);
expect(screen.getByRole("button", { name: /Calculate/i })).toBeInTheDocument();
});
});

17
vitest.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
exclude: ["node_modules", "e2e/**"],
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

2
vitest.setup.ts Normal file
View File

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