diff --git a/packages/core-typescript/vitest.base.jsdom.ts b/packages/core-typescript/vitest.base.jsdom.ts
index 44f5162..35ef434 100644
--- a/packages/core-typescript/vitest.base.jsdom.ts
+++ b/packages/core-typescript/vitest.base.jsdom.ts
@@ -1,5 +1,5 @@
import { defineConfig, mergeConfig } from "vitest/config";
-import { nodeVitestConfig } from "./vitest.base.node.js";
+import { nodeVitestConfig } from "./vitest.base.node.ts";
export const jsdomVitestConfig = mergeConfig(
nodeVitestConfig,
diff --git a/packages/core-ui/package.json b/packages/core-ui/package.json
index cc41124..e9354e2 100644
--- a/packages/core-ui/package.json
+++ b/packages/core-ui/package.json
@@ -10,7 +10,8 @@
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
- "typecheck": "tsc --noEmit"
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run --passWithNoTests"
},
"dependencies": {
"clsx": "^2.1.1",
@@ -19,8 +20,14 @@
},
"devDependencies": {
"@repo/core-eslint": "workspace:*",
+ "@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@storybook/react": "^8.6.0",
- "@types/react": "^19.0.0"
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.0",
+ "@types/react": "^19.0.0",
+ "jsdom": "^25.0.0",
+ "vitest": "^3.0.0"
}
}
diff --git a/packages/core-ui/src/atoms/button/button.test.tsx b/packages/core-ui/src/atoms/button/button.test.tsx
new file mode 100644
index 0000000..bcf664f
--- /dev/null
+++ b/packages/core-ui/src/atoms/button/button.test.tsx
@@ -0,0 +1,42 @@
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "@repo/core-testing/react";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Button } from "./button";
+
+describe("Button", () => {
+ it("renders children inside a button", () => {
+ renderWithProviders();
+ expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
+ });
+
+ it("calls onClick when activated", async () => {
+ const handleClick = vi.fn();
+ renderWithProviders();
+ await userEvent.click(screen.getByRole("button", { name: "Go" }));
+ expect(handleClick).toHaveBeenCalledOnce();
+ });
+
+ it("applies the destructive variant class", () => {
+ renderWithProviders();
+ expect(screen.getByRole("button")).toHaveClass(/destructive/);
+ });
+
+ it("applies the lg size class", () => {
+ renderWithProviders();
+ expect(screen.getByRole("button")).toHaveClass(/h-11/);
+ });
+
+ it("disabled prop sets the attribute and prevents click handler", async () => {
+ const handleClick = vi.fn();
+ renderWithProviders(
+ ,
+ );
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ await userEvent.click(button);
+ expect(handleClick).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/core-ui/src/atoms/input/input.test.tsx b/packages/core-ui/src/atoms/input/input.test.tsx
new file mode 100644
index 0000000..aa49087
--- /dev/null
+++ b/packages/core-ui/src/atoms/input/input.test.tsx
@@ -0,0 +1,31 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "@repo/core-testing/react";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Input } from "./input";
+
+describe("Input", () => {
+ it("renders an input with the given placeholder", () => {
+ renderWithProviders();
+ expect(screen.getByPlaceholderText("email")).toBeInTheDocument();
+ });
+
+ it("forwards the type attribute", () => {
+ renderWithProviders();
+ expect(screen.getByPlaceholderText("email")).toHaveAttribute("type", "email");
+ });
+
+ it("accepts user input", async () => {
+ renderWithProviders();
+ const input = screen.getByPlaceholderText("email");
+ await userEvent.type(input, "hi@example.com");
+ expect(input).toHaveValue("hi@example.com");
+ });
+
+ it("disabled prop blocks user input", async () => {
+ renderWithProviders();
+ const input = screen.getByPlaceholderText("email");
+ await userEvent.type(input, "hi");
+ expect(input).toHaveValue("");
+ });
+});
diff --git a/packages/core-ui/src/atoms/label/label.test.tsx b/packages/core-ui/src/atoms/label/label.test.tsx
new file mode 100644
index 0000000..d48b31d
--- /dev/null
+++ b/packages/core-ui/src/atoms/label/label.test.tsx
@@ -0,0 +1,16 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "@repo/core-testing/react";
+import { screen } from "@testing-library/react";
+import { Label } from "./label";
+
+describe("Label", () => {
+ it("renders the label text", () => {
+ renderWithProviders();
+ expect(screen.getByText("Email")).toBeInTheDocument();
+ });
+
+ it("forwards htmlFor to associate with an input", () => {
+ renderWithProviders();
+ expect(screen.getByText("Email")).toHaveAttribute("for", "email");
+ });
+});
diff --git a/packages/core-ui/src/molecules/form-field/form-field.test.tsx b/packages/core-ui/src/molecules/form-field/form-field.test.tsx
new file mode 100644
index 0000000..925ecf3
--- /dev/null
+++ b/packages/core-ui/src/molecules/form-field/form-field.test.tsx
@@ -0,0 +1,33 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "@repo/core-testing/react";
+import { screen } from "@testing-library/react";
+import { FormField } from "./form-field";
+
+describe("FormField", () => {
+ it("renders label associated with input via auto-derived id", () => {
+ renderWithProviders();
+ const input = screen.getByLabelText("Email Address");
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveAttribute("id", "email-address");
+ });
+
+ it("uses an explicit id over the derived one", () => {
+ renderWithProviders();
+ expect(screen.getByLabelText("Email")).toHaveAttribute("id", "custom-id");
+ });
+
+ it("renders a description when provided", () => {
+ renderWithProviders();
+ expect(screen.getByText("We never share it.")).toBeInTheDocument();
+ });
+
+ it("renders an error message when provided", () => {
+ renderWithProviders();
+ expect(screen.getByText("Required")).toBeInTheDocument();
+ });
+
+ it("forwards input props (e.g., type) to the underlying input", () => {
+ renderWithProviders();
+ expect(screen.getByLabelText("Email")).toHaveAttribute("type", "email");
+ });
+});
diff --git a/packages/core-ui/tsconfig.json b/packages/core-ui/tsconfig.json
index 4b07603..741720b 100644
--- a/packages/core-ui/tsconfig.json
+++ b/packages/core-ui/tsconfig.json
@@ -1,10 +1,12 @@
{
- "extends": "@repo/core-typescript/base.json",
+ "extends": "@repo/core-typescript/react-library.json",
"compilerOptions": {
"outDir": "dist",
- "rootDir": "src",
- "lib": ["ES2022", "DOM"],
- "jsx": "preserve"
+ "rootDir": ".",
+ "types": ["vitest/globals", "@testing-library/jest-dom"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
diff --git a/packages/core-ui/vitest.config.ts b/packages/core-ui/vitest.config.ts
new file mode 100644
index 0000000..8f3f7d7
--- /dev/null
+++ b/packages/core-ui/vitest.config.ts
@@ -0,0 +1,9 @@
+import path from "node:path";
+import { mergeConfig } from "vitest/config";
+import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
+
+export default mergeConfig(jsdomVitestConfig, {
+ resolve: {
+ alias: { "@": path.resolve(__dirname, "./src") },
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20049ea..f7dc5f9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -545,15 +545,33 @@ importers:
'@repo/core-eslint':
specifier: workspace:*
version: link:../core-eslint
+ '@repo/core-testing':
+ specifier: workspace:*
+ version: link:../core-testing
'@repo/core-typescript':
specifier: workspace:*
version: link:../core-typescript
'@storybook/react':
specifier: ^8.6.0
version: 8.6.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@8.6.18(prettier@3.8.1))(typescript@5.9.3)
+ '@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/react':
specifier: ^19.0.0
version: 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@25.5.2)(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/marketing-pages:
dependencies: