Files
community-rule/tests/utils/componentTestSuite.tsx
T
adilallo 9cb89162ab
CI Pipeline / test (pull_request) Successful in 7m5s
CI Pipeline / lint (pull_request) Has been cancelled
CI Pipeline / build (pull_request) Has been cancelled
CI Pipeline / e2e (webkit) (pull_request) Has been cancelled
CI Pipeline / e2e (chromium) (pull_request) Successful in 54m11s
CI Pipeline / e2e (firefox) (pull_request) Failing after 22m9s
CI Pipeline / visual-regression (pull_request) Successful in 11m50s
CI Pipeline / performance (pull_request) Successful in 13m59s
Fix TypeScript matcher typing issue
2026-01-28 15:57:47 -07:00

225 lines
5.8 KiB
TypeScript

import React from "react";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
type TestCases = {
renders?: boolean;
accessibility?: boolean;
keyboardNavigation?: boolean;
disabledState?: boolean;
errorState?: boolean;
};
type StateConfig<TProps> = {
disabledProps?: Partial<TProps>;
errorProps?: Partial<TProps>;
};
export interface ComponentTestSuiteConfig<TProps> {
/**
* React component under test.
*/
component: React.ComponentType<TProps>;
/**
* 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<TProps>;
/**
* 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<TProps>;
}
/**
* 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<TProps>(
config: ComponentTestSuiteConfig<TProps>,
) {
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(<Component {...props} />);
});
}
if (requiredProps.length > 0) {
it("honors required props", () => {
render(<Component {...props} />);
for (const key of requiredProps) {
const value = (props as Record<string, unknown>)[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(<Component {...props} />);
// Render again with optional props omitted to ensure no runtime error
const { unmount } = render(
<Component
{...({
...props,
...Object.fromEntries(
Object.keys(optionalProps).map((k) => [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(<Component {...props} />);
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(<Component {...props} />);
// 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(
<Component
{...({
...props,
...states.disabledProps,
} as TProps)}
/>,
);
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(
<Component
{...({
...props,
...states.errorProps,
} as TProps)}
/>,
);
});
}
});
}