From 11b5b15105b2840bc48f68b7190544347edae886 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 5 May 2026 16:21:58 +0200 Subject: [PATCH] feat(core-ui): add jsdom Vitest config + RTL tests for components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/core-typescript/vitest.base.jsdom.ts | 2 +- packages/core-ui/package.json | 11 ++++- .../core-ui/src/atoms/button/button.test.tsx | 42 +++++++++++++++++++ .../core-ui/src/atoms/input/input.test.tsx | 31 ++++++++++++++ .../core-ui/src/atoms/label/label.test.tsx | 16 +++++++ .../molecules/form-field/form-field.test.tsx | 33 +++++++++++++++ packages/core-ui/tsconfig.json | 10 +++-- packages/core-ui/vitest.config.ts | 9 ++++ pnpm-lock.yaml | 18 ++++++++ 9 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 packages/core-ui/src/atoms/button/button.test.tsx create mode 100644 packages/core-ui/src/atoms/input/input.test.tsx create mode 100644 packages/core-ui/src/atoms/label/label.test.tsx create mode 100644 packages/core-ui/src/molecules/form-field/form-field.test.tsx create mode 100644 packages/core-ui/vitest.config.ts 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: