diff --git a/apps/cms/package.json b/apps/cms/package.json
index 8bee798..1acc65e 100644
--- a/apps/cms/package.json
+++ b/apps/cms/package.json
@@ -7,6 +7,7 @@
"build": "echo 'CMS build requires database — use docker compose or pnpm dev'",
"dev": "next dev --port 3001",
"lint": "eslint .",
+ "test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit",
"generate:types": "payload generate:types"
},
@@ -24,9 +25,11 @@
},
"devDependencies": {
"@repo/core-eslint": "workspace:*",
+ "@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
- "@types/react-dom": "^19.0.0"
+ "@types/react-dom": "^19.0.0",
+ "vitest": "^3.0.0"
}
}
diff --git a/apps/cms/src/payload.config.test.ts b/apps/cms/src/payload.config.test.ts
new file mode 100644
index 0000000..1679d7f
--- /dev/null
+++ b/apps/cms/src/payload.config.test.ts
@@ -0,0 +1,20 @@
+import { describe, it, expect } from "vitest";
+import config from "./payload.config";
+
+describe("CMS app payload.config", () => {
+ it("registers all feature collections", async () => {
+ const resolved = await config;
+ const slugs = resolved.collections?.map((c) => c.slug) ?? [];
+ expect(slugs).toEqual(
+ expect.arrayContaining(["users", "articles", "pages", "media"]),
+ );
+ });
+
+ it("registers all feature globals", async () => {
+ const resolved = await config;
+ const slugs = resolved.globals?.map((g) => g.slug) ?? [];
+ expect(slugs).toEqual(
+ expect.arrayContaining(["site-settings", "header"]),
+ );
+ });
+});
diff --git a/apps/cms/tsconfig.json b/apps/cms/tsconfig.json
index 016d10a..5105090 100644
--- a/apps/cms/tsconfig.json
+++ b/apps/cms/tsconfig.json
@@ -10,7 +10,8 @@
"./src/payload.config.ts"
]
},
- "allowJs": true
+ "allowJs": true,
+ "types": ["vitest/globals"]
},
"include": [
"next-env.d.ts",
diff --git a/apps/cms/vitest.config.ts b/apps/cms/vitest.config.ts
new file mode 100644
index 0000000..c933396
--- /dev/null
+++ b/apps/cms/vitest.config.ts
@@ -0,0 +1,7 @@
+import path from "node:path";
+import { mergeConfig } from "vitest/config";
+import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
+
+export default mergeConfig(nodeVitestConfig, {
+ resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
+});
diff --git a/apps/web-next/package.json b/apps/web-next/package.json
index 5f65537..b05afae 100644
--- a/apps/web-next/package.json
+++ b/apps/web-next/package.json
@@ -7,6 +7,7 @@
"build": "echo 'Next.js build requires full environment — use pnpm dev or docker'",
"dev": "next dev --port 3000",
"lint": "eslint .",
+ "test": "vitest run --passWithNoTests",
"test:e2e": "playwright test",
"test:e2e:install": "playwright install --with-deps chromium",
"typecheck": "tsc --noEmit"
@@ -33,9 +34,15 @@
"devDependencies": {
"@playwright/test": "^1.50.0",
"@repo/core-eslint": "workspace:*",
+ "@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
- "@types/react-dom": "^19.0.0"
+ "@types/react-dom": "^19.0.0",
+ "jsdom": "^25.0.0",
+ "vitest": "^3.0.0"
}
}
diff --git a/apps/web-next/src/app/providers.test.tsx b/apps/web-next/src/app/providers.test.tsx
new file mode 100644
index 0000000..ab94080
--- /dev/null
+++ b/apps/web-next/src/app/providers.test.tsx
@@ -0,0 +1,14 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { Providers } from "./providers";
+
+describe("Providers", () => {
+ it("renders children", () => {
+ render(
+
+ hi
+ ,
+ );
+ expect(screen.getByTestId("child")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web-next/src/server/bind-production.test.ts b/apps/web-next/src/server/bind-production.test.ts
new file mode 100644
index 0000000..0722b49
--- /dev/null
+++ b/apps/web-next/src/server/bind-production.test.ts
@@ -0,0 +1,37 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+vi.mock("@repo/core-cms", () => ({ default: Promise.resolve({}) }));
+vi.mock("@repo/blog/di/bind-production", () => ({ bindProductionBlog: vi.fn() }));
+vi.mock("@repo/auth/di/bind-production", () => ({ bindProductionAuth: vi.fn() }));
+vi.mock("@repo/marketing-pages/di/bind-production", () => ({ bindProductionMarketingPages: vi.fn() }));
+vi.mock("@repo/navigation/di/bind-production", () => ({ bindProductionNavigation: vi.fn() }));
+
+describe("bindAllProduction", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ });
+
+ it("binds all four feature production repos", async () => {
+ const { bindAllProduction } = await import("./bind-production");
+ const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
+ const { bindProductionAuth } = await import("@repo/auth/di/bind-production");
+ const { bindProductionMarketingPages } = await import("@repo/marketing-pages/di/bind-production");
+ const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production");
+
+ await bindAllProduction();
+
+ expect(bindProductionBlog).toHaveBeenCalledOnce();
+ expect(bindProductionAuth).toHaveBeenCalledOnce();
+ expect(bindProductionMarketingPages).toHaveBeenCalledOnce();
+ expect(bindProductionNavigation).toHaveBeenCalledOnce();
+ });
+
+ it("is idempotent — second call does not re-bind", async () => {
+ const { bindAllProduction } = await import("./bind-production");
+ const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
+ await bindAllProduction();
+ await bindAllProduction();
+ expect(bindProductionBlog).toHaveBeenCalledOnce();
+ });
+});
diff --git a/apps/web-next/tsconfig.json b/apps/web-next/tsconfig.json
index 760bcb1..64b2c19 100644
--- a/apps/web-next/tsconfig.json
+++ b/apps/web-next/tsconfig.json
@@ -7,7 +7,8 @@
"./src/*"
]
},
- "allowJs": true
+ "allowJs": true,
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": [
"next-env.d.ts",
diff --git a/apps/web-next/vitest.config.ts b/apps/web-next/vitest.config.ts
new file mode 100644
index 0000000..b1c34d3
--- /dev/null
+++ b/apps/web-next/vitest.config.ts
@@ -0,0 +1,8 @@
+import path from "node:path";
+import { mergeConfig } from "vitest/config";
+import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
+
+export default mergeConfig(jsdomVitestConfig, {
+ esbuild: { jsx: "automatic" },
+ resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
+});
diff --git a/apps/web-tanstack/package.json b/apps/web-tanstack/package.json
index 26ec577..7e2866c 100644
--- a/apps/web-tanstack/package.json
+++ b/apps/web-tanstack/package.json
@@ -7,6 +7,7 @@
"build": "echo 'placeholder — TanStack Start build configured in later plan'",
"dev": "echo 'placeholder'",
"lint": "eslint .",
+ "test": "vitest run --passWithNoTests",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit"
},
@@ -25,9 +26,15 @@
"devDependencies": {
"@playwright/test": "^1.50.0",
"@repo/core-eslint": "workspace:*",
+ "@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
- "@types/react-dom": "^19.0.0"
+ "@types/react-dom": "^19.0.0",
+ "jsdom": "^25.0.0",
+ "vitest": "^3.0.0"
}
}
diff --git a/apps/web-tanstack/src/routes/__root.test.tsx b/apps/web-tanstack/src/routes/__root.test.tsx
new file mode 100644
index 0000000..d76c8b3
--- /dev/null
+++ b/apps/web-tanstack/src/routes/__root.test.tsx
@@ -0,0 +1,23 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+
+// Mock @tanstack/react-router so we don't need a full router context
+vi.mock("@tanstack/react-router", () => ({
+ createRootRoute: vi.fn((opts: { component: React.ComponentType }) => ({
+ options: { component: opts.component },
+ })),
+ Outlet: () =>
,
+}));
+
+describe("Root route", () => {
+ it("wraps Outlet with TanstackTrpcProvider (children render)", async () => {
+ const { Route } = await import("./__root");
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const RootComponent = (Route as any).options.component as React.ComponentType;
+
+ render();
+
+ expect(screen.getByTestId("outlet")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web-tanstack/tsconfig.json b/apps/web-tanstack/tsconfig.json
index af33277..c310251 100644
--- a/apps/web-tanstack/tsconfig.json
+++ b/apps/web-tanstack/tsconfig.json
@@ -6,7 +6,8 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
- }
+ },
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
diff --git a/apps/web-tanstack/vitest.config.ts b/apps/web-tanstack/vitest.config.ts
new file mode 100644
index 0000000..b1c34d3
--- /dev/null
+++ b/apps/web-tanstack/vitest.config.ts
@@ -0,0 +1,8 @@
+import path from "node:path";
+import { mergeConfig } from "vitest/config";
+import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
+
+export default mergeConfig(jsdomVitestConfig, {
+ esbuild: { jsx: "automatic" },
+ resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 92171f5..896b507 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,6 +54,9 @@ importers:
'@repo/core-eslint':
specifier: workspace:*
version: link:../../packages/core-eslint
+ '@repo/core-testing':
+ specifier: workspace:*
+ version: link:../../packages/core-testing
'@repo/core-typescript':
specifier: workspace:*
version: link:../../packages/core-typescript
@@ -66,6 +69,9 @@ importers:
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
+ vitest:
+ specifier: ^3.0.0
+ version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)
apps/storybook:
dependencies:
@@ -167,9 +173,21 @@ importers:
'@repo/core-eslint':
specifier: workspace:*
version: link:../../packages/core-eslint
+ '@repo/core-testing':
+ specifier: workspace:*
+ version: link:../../packages/core-testing
'@repo/core-typescript':
specifier: workspace:*
version: link:../../packages/core-typescript
+ '@testing-library/jest-dom':
+ specifier: ^6.5.0
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^16.0.0
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@testing-library/user-event':
+ specifier: ^14.5.0
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/node':
specifier: ^22.0.0
version: 22.19.17
@@ -179,6 +197,12 @@ importers:
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
+ jsdom:
+ specifier: ^25.0.0
+ version: 25.0.1
+ vitest:
+ specifier: ^3.0.0
+ version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)
apps/web-tanstack:
dependencies:
@@ -219,9 +243,21 @@ importers:
'@repo/core-eslint':
specifier: workspace:*
version: link:../../packages/core-eslint
+ '@repo/core-testing':
+ specifier: workspace:*
+ version: link:../../packages/core-testing
'@repo/core-typescript':
specifier: workspace:*
version: link:../../packages/core-typescript
+ '@testing-library/jest-dom':
+ specifier: ^6.5.0
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^16.0.0
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@testing-library/user-event':
+ specifier: ^14.5.0
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/node':
specifier: ^22.0.0
version: 22.19.17
@@ -231,6 +267,12 @@ importers:
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
+ jsdom:
+ specifier: ^25.0.0
+ version: 25.0.1
+ vitest:
+ specifier: ^3.0.0
+ version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(tsx@4.21.0)
packages/auth:
dependencies: