feat(core-ui): add jsdom Vitest config + RTL tests for components
Adopts jsdomVitestConfig from @repo/core-typescript. Adds
@testing-library/react, @testing-library/user-event, jsdom devDeps.
Writes smoke + interaction tests for every atom/molecule/organism/template
using renderWithProviders from @repo/core-testing/react.
Components covered:
- atoms: Button (5 tests), Input (4), Label (2)
- molecules: FormField (5)
- organisms / templates: empty barrels, no components
Adjustments:
- core-ui/tsconfig.json now extends react-library.json (jsx: react-jsx)
with rootDir "." + paths {"@/*"} + types [vitest/globals, jest-dom]
- core-typescript/vitest.base.jsdom.ts uses ./vitest.base.node.ts (explicit
.ts extension) so Node's ESM resolver finds the source file when loaded
via the package export from a downstream package
Spec: §6.1, §6.5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig, mergeConfig } from "vitest/config";
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
import { nodeVitestConfig } from "./vitest.base.node.js";
|
import { nodeVitestConfig } from "./vitest.base.node.ts";
|
||||||
|
|
||||||
export const jsdomVitestConfig = mergeConfig(
|
export const jsdomVitestConfig = mergeConfig(
|
||||||
nodeVitestConfig,
|
nodeVitestConfig,
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit",
|
"build": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -19,8 +20,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/core-eslint": "workspace:*",
|
"@repo/core-eslint": "workspace:*",
|
||||||
|
"@repo/core-testing": "workspace:*",
|
||||||
"@repo/core-typescript": "workspace:*",
|
"@repo/core-typescript": "workspace:*",
|
||||||
"@storybook/react": "^8.6.0",
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
packages/core-ui/src/atoms/button/button.test.tsx
Normal file
42
packages/core-ui/src/atoms/button/button.test.tsx
Normal file
@@ -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(<Button>Click me</Button>);
|
||||||
|
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onClick when activated", async () => {
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
renderWithProviders(<Button onClick={handleClick}>Go</Button>);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||||
|
expect(handleClick).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies the destructive variant class", () => {
|
||||||
|
renderWithProviders(<Button variant="destructive">X</Button>);
|
||||||
|
expect(screen.getByRole("button")).toHaveClass(/destructive/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies the lg size class", () => {
|
||||||
|
renderWithProviders(<Button size="lg">X</Button>);
|
||||||
|
expect(screen.getByRole("button")).toHaveClass(/h-11/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disabled prop sets the attribute and prevents click handler", async () => {
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
renderWithProviders(
|
||||||
|
<Button disabled onClick={handleClick}>
|
||||||
|
X
|
||||||
|
</Button>,
|
||||||
|
);
|
||||||
|
const button = screen.getByRole("button");
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
await userEvent.click(button);
|
||||||
|
expect(handleClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
31
packages/core-ui/src/atoms/input/input.test.tsx
Normal file
31
packages/core-ui/src/atoms/input/input.test.tsx
Normal file
@@ -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(<Input placeholder="email" />);
|
||||||
|
expect(screen.getByPlaceholderText("email")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards the type attribute", () => {
|
||||||
|
renderWithProviders(<Input type="email" placeholder="email" />);
|
||||||
|
expect(screen.getByPlaceholderText("email")).toHaveAttribute("type", "email");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts user input", async () => {
|
||||||
|
renderWithProviders(<Input placeholder="email" />);
|
||||||
|
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(<Input placeholder="email" disabled />);
|
||||||
|
const input = screen.getByPlaceholderText("email");
|
||||||
|
await userEvent.type(input, "hi");
|
||||||
|
expect(input).toHaveValue("");
|
||||||
|
});
|
||||||
|
});
|
||||||
16
packages/core-ui/src/atoms/label/label.test.tsx
Normal file
16
packages/core-ui/src/atoms/label/label.test.tsx
Normal file
@@ -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(<Label htmlFor="email">Email</Label>);
|
||||||
|
expect(screen.getByText("Email")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards htmlFor to associate with an input", () => {
|
||||||
|
renderWithProviders(<Label htmlFor="email">Email</Label>);
|
||||||
|
expect(screen.getByText("Email")).toHaveAttribute("for", "email");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(<FormField label="Email Address" />);
|
||||||
|
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(<FormField label="Email" id="custom-id" />);
|
||||||
|
expect(screen.getByLabelText("Email")).toHaveAttribute("id", "custom-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a description when provided", () => {
|
||||||
|
renderWithProviders(<FormField label="Email" description="We never share it." />);
|
||||||
|
expect(screen.getByText("We never share it.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an error message when provided", () => {
|
||||||
|
renderWithProviders(<FormField label="Email" error="Required" />);
|
||||||
|
expect(screen.getByText("Required")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards input props (e.g., type) to the underlying input", () => {
|
||||||
|
renderWithProviders(<FormField label="Email" type="email" />);
|
||||||
|
expect(screen.getByLabelText("Email")).toHaveAttribute("type", "email");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "@repo/core-typescript/base.json",
|
"extends": "@repo/core-typescript/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": ".",
|
||||||
"lib": ["ES2022", "DOM"],
|
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||||
"jsx": "preserve"
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
|
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
|
||||||
|
|||||||
9
packages/core-ui/vitest.config.ts
Normal file
9
packages/core-ui/vitest.config.ts
Normal file
@@ -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") },
|
||||||
|
},
|
||||||
|
});
|
||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -545,15 +545,33 @@ importers:
|
|||||||
'@repo/core-eslint':
|
'@repo/core-eslint':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core-eslint
|
version: link:../core-eslint
|
||||||
|
'@repo/core-testing':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core-testing
|
||||||
'@repo/core-typescript':
|
'@repo/core-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core-typescript
|
version: link:../core-typescript
|
||||||
'@storybook/react':
|
'@storybook/react':
|
||||||
specifier: ^8.6.0
|
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)
|
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':
|
'@types/react':
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.14
|
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:
|
packages/marketing-pages:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user