Adds Vitest and Playwright testing setup with sample tests
Some checks failed
Lint / Lint and Typecheck (push) Failing after 27s
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:
33
.cursorrules
Normal file
33
.cursorrules
Normal 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
5
.gitignore
vendored
@@ -43,4 +43,7 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# idea files
|
# idea files
|
||||||
.idea
|
.idea
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
test-results/
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -104,6 +104,24 @@ To run locally:
|
|||||||
```
|
```
|
||||||
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.
|
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
|
## ✏️ Inputs & Variables
|
||||||
|
|||||||
15
e2e/home.spec.ts
Normal file
15
e2e/home.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
|
||||||
14
package.json
14
package.json
@@ -11,7 +11,9 @@
|
|||||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"lint": "next typegen && eslint . && npx tsc --noEmit",
|
"lint": "next typegen && eslint . && npx tsc --noEmit",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
@@ -34,20 +36,28 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.56.1",
|
||||||
"@tailwindcss/postcss": "4.1.17",
|
"@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/node": "24.10.1",
|
||||||
"@types/react": "19.2.6",
|
"@types/react": "19.2.6",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.3",
|
"eslint-config-next": "16.0.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"jsdom": "^27.2.0",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "0.7.1",
|
"prettier-plugin-tailwindcss": "0.7.1",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.17",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.47.0"
|
"typescript-eslint": "8.47.0",
|
||||||
|
"vitest": "^4.0.13"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
|
|||||||
34
playwright.config.ts
Normal file
34
playwright.config.ts
Normal 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
1496
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
27
src/app/components/__tests__/FireCalculatorForm.test.tsx
Normal file
27
src/app/components/__tests__/FireCalculatorForm.test.tsx
Normal 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
17
vitest.config.ts
Normal 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
2
vitest.setup.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
Reference in New Issue
Block a user