import React from "react"; import { describe, it, expect } from "vitest"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { axe } from "jest-axe"; import { renderWithProviders as render } from "./test-utils"; type TestCases = { renders?: boolean; accessibility?: boolean; keyboardNavigation?: boolean; disabledState?: boolean; errorState?: boolean; }; type StateConfig = { disabledProps?: Partial; errorProps?: Partial; }; export interface ComponentTestSuiteConfig { /** * React component under test. */ component: React.ComponentType; /** * Human-readable name for the suite (usually the component name). */ name: string; /** * Default props used for baseline rendering. */ props: TProps; /** * Props that are considered required for the component to behave correctly. * Used for simple sanity checks (e.g., does label text render). */ requiredProps?: (keyof TProps)[]; /** * Optional props that should not cause the component to break when omitted. */ optionalProps?: Partial; /** * Primary ARIA role for the main interactive element. * Used for generic keyboardNavigation and accessibility checks. * * Examples: "button", "textbox", "checkbox", "radio", "combobox". */ primaryRole?: string; /** * Which standard tests to run for this component. */ testCases?: TestCases; /** * State-specific props for disabled/error tests. */ states?: StateConfig; } /** * Standardized component test suite. * * Usage: * componentTestSuite({ * component: Button, * name: "Button", * props: { children: "Click me" }, * requiredProps: ["children"], * primaryRole: "button", * testCases: { * renders: true, * accessibility: true, * keyboardNavigation: true, * disabledState: true, * }, * states: { * disabledProps: { disabled: true }, * }, * }); */ export function componentTestSuite( config: ComponentTestSuiteConfig, ) { const { component: Component, name, props, requiredProps = [], optionalProps, primaryRole = "button", testCases = { renders: true, accessibility: true, keyboardNavigation: true, disabledState: true, errorState: false, }, states = {}, } = config; describe(`${name} (standard suite)`, () => { if (testCases.renders) { it("renders without crashing", () => { render(); }); } if (requiredProps.length > 0) { it("honors required props", () => { render(); for (const key of requiredProps) { const value = (props as Record)[key as string]; expect( value, `Expected required prop "${String(key)}" to be defined`, ).toBeDefined(); } }); } if (optionalProps) { it("handles optional props gracefully when omitted", () => { // Render with all props render(); // Render again with optional props omitted to ensure no runtime error const { unmount } = render( [k, undefined]), ), } as TProps)} />, ); // Basic sanity check: component is mounted // (we don't assert specific DOM for optional props generically) expect(unmount).toBeDefined(); }); } if (testCases.accessibility) { it("has no obvious accessibility violations (axe)", async () => { const { container } = render(); const results = await axe(container); // Avoid relying on Jest matcher typings in Vitest/Next typecheck context. expect(results.violations).toHaveLength(0); }); } if (testCases.keyboardNavigation) { it("supports basic keyboard navigation (Tab + Enter/Space)", async () => { const user = userEvent.setup(); render(); // Focus the primary interactive element by role const interactive = screen.queryByRole(primaryRole as never) ?? // Fallback: first button if specified role is not found screen.getByRole("button"); interactive.focus(); expect(interactive).toHaveFocus(); // Trigger activation via keyboard await user.keyboard("{Enter}"); await user.keyboard(" "); // Still in the document after interaction expect(interactive).toBeInTheDocument(); }); } if (testCases.disabledState && states.disabledProps) { it("handles disabled state correctly", async () => { const user = userEvent.setup(); render( , ); const interactive = screen.queryByRole(primaryRole as never) ?? screen.getByRole("button"); // If the component exposes disabled via attribute, assert it if ("disabled" in interactive) { expect(interactive).toHaveAttribute("disabled"); } // Attempt interaction; should not throw or cause obvious change await user.click(interactive); expect(interactive).toBeInTheDocument(); }); } if (testCases.errorState && states.errorProps) { it("handles error state without crashing", () => { // Render with error props applied; no additional assertions to keep this generic render( , ); }); } }); }