en un clic
gen-rtl-test
// Generate React Testing Library tests following OCP Console best practices
// Generate React Testing Library tests following OCP Console best practices
Audit all Claude Code skills for stale references, broken paths, and deprecated tool names
Comprehensive local code review using both Claude AI and CodeRabbit AI before pushing changes to GitHub. This command analyzes your local changes and provides actionable feedback without posting anything to GitHub.
Expert package update assistant for OpenShift Console. Update packages safely with automated testing, building, and fixing.
Automated QA verification for OpenShift Console PRs. Builds and runs the console on both main and the PR branch, captures before/after screenshots and GIFs via Playwright MCP, then posts a side-by-side comparison as a GitHub PR comment. Use when the user asks to verify a PR, QA changes, capture visual proof, or show before/after evidence.
Debug and fix failing Playwright e2e tests with MCP-assisted diagnosis. Use when user says "playwright test failing", "fix e2e test", "debug spec", or provides a failing .spec.ts file, e2e directory, or Playwright tag.
Migrate a Cypress test file to Playwright following Console's architecture. Use when user says "migrate", "convert cypress", "port to playwright", or provides a .cy.ts or .feature file path.
| name | gen-rtl-test |
| description | Generate React Testing Library tests following OCP Console best practices |
| argument-hint | [path/to/Component.tsx] or use @file for autocomplete |
Usage:
/gen-rtl-test - Default: Automatically checks git diff for component changes and generates tests/gen-rtl-test path/to/Component.tsx - Generate tests for a specific component/gen-rtl-test @Component.tsx - Use @ for file autocomplete, then select the fileWhen invoked without arguments, the slash command follows this intelligent workflow:
git diff --name-only to find modified files.tsx and .jsx component files (exclude test files, type files, utils)This workflow ensures you automatically generate tests for components you're actively working on.
You are helping generate comprehensive React Testing Library (RTL) test cases following the established OCP Console unit testing standards.
Before writing imports: Inspect the component under test (and its hooks). Use renderWithProviders only if it depends on the Redux store and/or React Router. Otherwise use render from @testing-library/react. (See Rule 0 for the full table, including PluginStore when relevant.)
This guide establishes a consistent, project-wide standard for all React component tests in the OCP Console.
Core Philosophy: Test component behavior from a user's perspective, not internal implementation details.
renderWithProviders Only When the Component Needs Redux and/or RouterPick the render helper from what the component under test actually uses:
| Use | When the component (or non-mocked hooks it calls) … |
|---|---|
renderWithProviders from @console/shared/src/test-utils/unit-test-utils | Needs the Redux store (e.g. useSelector, useDispatch, k8s/resource hooks backed by the console store) and/or React Router (e.g. useNavigate, useParams, useLocation, Link, NavLink). The helper also wraps PluginStore — use it when the tree touches dynamic plugin APIs that expect that context. |
render from @testing-library/react | Has no Redux or Router dependency (pure presentational UI, local useState only, or all store/router hooks are mocked so the real provider is unnecessary). |
// Standalone / presentational — no Redux or Router in the component under test
import { render, screen } from '@testing-library/react';
import { BadgeLabel } from './BadgeLabel';
it('renders the label', () => {
render(<BadgeLabel text="Ready" />);
expect(screen.getByText('Ready')).toBeVisible();
});
// Connected to store and/or routes — use the console test wrapper
import { screen } from '@testing-library/react';
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
import { DeploymentListRow } from './DeploymentListRow';
it('shows the deployment name', () => {
renderWithProviders(<DeploymentListRow deployment={mockDeployment} />);
expect(screen.getByRole('cell', { name: /nginx/i })).toBeVisible();
});
Why renderWithProviders exists: it supplies Redux Provider, MemoryRouter, and PluginStore so typical console components do not throw when mounting.
Optional clarity for reviewers: If the file uses render (not renderWithProviders), a one-line comment at the top of the file or above the first test can help reviewers, e.g. // Unit tests: component has no Redux or Router dependencies.
Do not use renderWithProviders “by default” for every console file — that hides missing providers in tests that should be asserting integration with real store/router behavior, and it adds cost where render is enough.
ALWAYS use userEvent from @testing-library/user-event for user interactions.
// FORBIDDEN — ESLint / project rules (e.g. testing-library) when enabled
import { fireEvent } from '@testing-library/react';
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'test' } });
// REQUIRED
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'test');
Why:
userEvent simulates real user behavior (focus, blur, keyboard events)fireEvent dispatches raw DOM events (not realistic)userEvent catches more bugs related to event handlingawaitALWAYS use screen from RTL instead of destructuring queries from render.
// FORBIDDEN — ESLint (e.g. testing-library/prefer-screen-queries) when enabled
const { getByRole, getByText } = render(<MyComponent />);
const button = getByRole('button');
// REQUIRED — use whichever render helper matches Rule 0, then always query via screen
import { render, screen } from '@testing-library/react';
// or: import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
render(<MyComponent />); // or renderWithProviders(<MyComponent />) when Redux/Router are needed
const button = screen.getByRole('button');
Why:
screen.debug()testing-library/prefer-screen-queries enforces thisRTL emphasizes testing components as users interact with them. Users find buttons by visible text (e.g., "Submit"), not by CSS classes, IDs, or test IDs. Therefore, test selectors should prioritize what users see and interact with.
User-Centric Testing - Test what users see and interact with. DO NOT test:
expect(container.firstChild).toBe...)Accessibility-First - Queries match how screen readers and users interact with the UI
Semantic Over Generic - Always prefer role-based queries (e.g., getByRole) over generic selectors
DRY Helpers - Use reusable function in frontend/packages/console-shared/src/test-utils directoty and sub-directory if exists else extract repetitive setup into reusable functions
Async-Aware - Handle asynchronous updates with findBy* and waitFor
TypeScript Safety - Use proper types for props, state, and mock data
Arrange-Act-Assert (AAA) Pattern - Structure tests logically:
This is the #1 most critical rule for test generation.
NEVER use require() in test files. NO EXCEPTIONS.
❌ FORBIDDEN - In test bodies:
it('should work', () => {
const { k8sCreate } = require('@console/internal/module/k8s'); // ❌ NEVER
});
❌ FORBIDDEN - In mock factories:
jest.mock('../Component', () => {
const React = require('react'); // ❌ NEVER - even here!
return () => React.createElement('div', null, 'Mock');
});
✅ REQUIRED - ES6 imports only:
// Import at file top
import { k8sCreate } from '@console/internal/module/k8s';
// Simple mocks - no React.createElement needed
jest.mock('../Component', () => () => null); // ✅ Return null
jest.mock('../LoadingSpinner', () => () => 'Loading...'); // ✅ Return string
// Use in tests
it('should work', () => {
(k8sCreate as jest.Mock).mockResolvedValue({});
});
Why ZERO tolerance:
require() breaks Jest's mock hoisting mechanismFile Structure:
MyComponentDirectory/
├── __tests__/
│ └── MyComponent.spec.tsx
└── MyComponent.tsx
__tests__/ directory within component directory.spec.tsx extensionBefore manually mocking, check __mocks__/ directory for existing global mocks (e.g., react-i18next, localStorage, k8sResourcesMocks). These are applied automatically.
Mock functions must NOT return JSX to avoid Jest hoisting errors:
// ✅ CORRECT:
jest.mock('../MyComponent', () => () => null);
// ✅ CORRECT:
jest.mock('../LoadingSpinner', () => () => 'Loading...');
// ✅ CORRECT: Return children directly
jest.mock('../utils/firehose', () => ({
Firehose: (props) => props.children,
}));
// ✅ CORRECT: Use jest.fn for tracking calls
jest.mock('../utils/firehose', () => ({
Firehose: jest.fn((props) => props.children),
}));
// ❌ INCORRECT (causes hoisting errors):
jest.mock('../MyComponent', () => () => <div>My Mock</div>);
jest.mock('../useCustomHook', () => ({
useCustomHook: jest.fn(() => [/* mock data */]),
}));
jest.mock('@console/internal/module/k8s', () => ({
...jest.requireActual('@console/internal/module/k8s'),
k8sCreate: jest.fn(),
k8sPatch: jest.fn(),
}));
import * as k8sModule from '@console/internal/module/k8s';
it('should do something when k8sGet succeeds', () => {
jest.spyOn(k8sModule, 'k8sGet').mockResolvedValue(data);
// ... rest of the test ...
});
DO NOT mock the useReduxStore hook. Instead, pass initialState to renderWithProviders:
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
it('should render with mock Redux data', () => {
const mockK8sState = { /* ... */ };
renderWithProviders(
<MyComponent />,
{
initialState: {
k8s: mockK8sState
}
}
);
expect(screen.getByText('My Mock Data')).toBeVisible();
});
STRICTLY ENFORCED - ZERO EXCEPTIONS
Always use ES6 import/export syntax in test files. NEVER use require() - not in test bodies, not in mock factories, NOWHERE.
✅ CORRECT - ES6 Imports:
// Import at the top of the file
import { k8sCreate } from '@console/internal/module/k8s';
import { history } from '@console/internal/components/utils';
import * as pdbModels from '../pdb-models';
// Simple mocks - return null or strings, NO React.createElement
jest.mock('../Component', () => () => null);
jest.mock('../ButtonBar', () => ({ children }) => children);
// Use in test
it('should create resource', async () => {
(k8sCreate as jest.Mock).mockResolvedValue({});
jest.spyOn(history, 'push');
jest.spyOn(pdbModels, 'patchPDB').mockResolvedValue({});
// ... rest of test
});
❌ INCORRECT - require() ANYWHERE:
// ❌ NEVER in test bodies
it('should create resource', async () => {
const { k8sCreate } = require('@console/internal/module/k8s'); // ❌ FORBIDDEN
});
// ❌ NEVER in mock factories
jest.mock('../Component', () => {
const React = require('react'); // ❌ FORBIDDEN - even here!
return () => React.createElement('div', null, 'Mock');
});
// ❌ NEVER in beforeEach
beforeEach(() => {
const utils = require('../utils'); // ❌ FORBIDDEN
});
How to avoid require() in mocks:
// ✅ Return null instead of JSX
jest.mock('../Component', () => () => null);
// ✅ Return string instead of JSX
jest.mock('../LoadingSpinner', () => () => 'Loading...');
// ✅ Return children directly
jest.mock('../Wrapper', () => ({ children }) => children);
// ✅ Use jest.fn for tracking
jest.mock('../ButtonBar', () => jest.fn(({ children }) => children));
Enforcement Checklist:
import statements at file toprequire() calls anywhere in the filejest.Mock when neededimport { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
// Top-level describe for the component
describe('MyComponent', () => {
// Nested describe for specific features
describe('when loading', () => {
it('should show the loading spinner', () => {
jest.spyOn(myHooksModule, 'useCustomHook').mockReturnValue({ isLoading: true });
render(<MyComponent />);
expect(screen.getByRole('progressbar')).toBeVisible();
});
it('should not show the data grid', () => {
jest.spyOn(myHooksModule, 'useCustomHook').mockReturnValue({ isLoading: true });
render(<MyComponent />);
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
});
});
describe('when data is loaded', () => {
it('should show the data grid', () => {
jest.spyOn(myHooksModule, 'useCustomHook').mockReturnValue({ isLoading: false, data: [...] });
render(<MyComponent />);
expect(screen.getByRole('grid')).toBeVisible();
});
});
});
Requirements:
describe block named after componentdescribe blocks for related testsit() method (not test())it block tests only a single state or interactionSame decision as Rule 0:
render from '@testing-library/react' — when the component under test has no Redux or React Router dependency (see Rule 0 table).renderWithProviders from '@console/shared/src/test-utils/unit-test-utils' — when it needs Redux and/or React Router (and use it when plugin context is required; see Rule 0).import { render, screen } from '@testing-library/react';
// ✅ DO: Use the global 'screen' object
it('should find the heading', () => {
render(<MyComponent />);
const heading = screen.getByRole('heading', { name: /welcome/i });
expect(heading).toBeVisible();
});
// ❌ AVOID: Destructuring queries from 'render'
it('should find the heading', () => {
const { getByRole } = render(<MyComponent />);
const heading = getByRole('heading', { name: /welcome/i });
expect(heading).toBeVisible();
});
Exception: Use within() for scoped queries or when you need container for specific assertions.
Query Priority (most to least preferred):
getByRolegetByLabelTextgetByPlaceholderTextgetByTextgetByDisplayValuegetByAltTextgetByTitlegetByTestId (last resort only). This might involve adding a data-test attribute to the implementation component element.Query Variants:
getBy* - Element expected to be present synchronously (throws if not found)queryBy* - Only for asserting element is NOT presentfindBy* - Element will appear asynchronously (returns Promise)Anti-pattern: Avoid container.querySelector - it tests implementation details.
Helpful Tip: For iframe or markdown content, use screen.getByRole('document').
i flag - When text spans multiple wrapper nodes (avoid case-insensitive matching)Note: Avoid case-insensitive matching based on Console UX text casing convention.
// ✅ GOOD: Tests accessible name + existence
expect(screen.getByRole('button', { name: 'Submit' })).toBeVisible();
// ❌ AVOID: Separate queries for same element
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(screen.getByText('Submit')).toBeInTheDocument();
When to use:
toBeVisible() - For elements users are expected to see or interact withtoBeInTheDocument() - For structural elements or conditional rendering verificationAnti-pattern: Avoid weak assertions like .toBeTruthy() or .toBeInTheDocument() for visible elements.
CRITICAL: When testing form input fields, ALWAYS use verifyInputField utility. This is strictly enforced.
Use verifyInputField when your test needs to verify:
*) is shownDO NOT manually write separate assertions for each of these - use the utility instead.
import { verifyInputField } from '@console/shared/src/test-utils/unit-test-utils';
// ✅ GOOD: Use verifyInputField for comprehensive field testing
it('should render the Name field with label, input, and help text', async () => {
render(<MyFormComponent />);
await verifyInputField({
inputLabel: 'Name',
containerId: 'test-name-form',
initialValue: 'test',
testValue: 'test',
helpText: 'Unique name for the resource',
isRequired: true,
});
});
// ✅ GOOD: Test multiple fields with verifyInputField
it('should render all form fields correctly', async () => {
render(<MyFormComponent />);
await verifyInputField({
inputLabel: 'Name',
containerId: 'test-name-form',
initialValue: '',
testValue: 'my-resource',
isRequired: true,
});
await verifyInputField({
inputLabel: 'Description',
containerId: 'test-description-form',
initialValue: '',
testValue: 'A description',
helpText: 'Optional description for this resource',
isRequired: false,
});
});
// ❌ BAD: Manual assertions for form fields
it('should render the Name field', async () => {
render(<MyFormComponent />);
// Don't do this - use verifyInputField instead!
expect(screen.getByLabelText('Name')).toBeInTheDocument();
expect(screen.getByLabelText('Name')).toHaveValue('');
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'test' } });
expect(screen.getByLabelText('Name')).toHaveValue('test');
expect(screen.getByText('Unique name for the resource')).toBeInTheDocument();
});
For these cases, use standard RTL queries.
When testing form components:
verifyInputField for each text input field testverifyInputField from '@console/shared/src/test-utils/unit-test-utils'import userEvent from '@testing-library/user-event';
it('should show content when expanded', async () => {
render(<Collapsible />);
const user = userEvent.setup();
// 1. Assert initial hidden state
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
// 2. Simulate user action
await user.click(screen.getByRole('button', { name: 'Expand' }));
// 3. Assert final visible state
expect(screen.getByText('Hidden content')).toBeVisible();
});
// Use findBy* to wait for an element to appear
const element = await screen.findByText('Loaded content');
expect(element).toBeVisible();
// Use waitFor for complex assertions
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
Avoid Explicit act(): Rarely needed. render, userEvent, findBy*, and waitFor already wrap operations in act().
describe('MyComponent', () => {
beforeEach(() => {
jest.spyOn(myHooksModule, 'useCustomHook').mockReturnValue({ isLoading: false });
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render the default state', () => {
render(<MyComponent />);
// ...
});
it('should render a different state', () => {
jest.spyOn(myHooksModule, 'useCustomHook').mockReturnValue({ isLoading: true });
render(<MyComponent />);
// ...
});
});
import { render, screen, within } from '@testing-library/react';
render(<MyDashboard />);
const userProfileCard = screen.getByTestId('profile-card');
// Scope queries to only that card
const userName = within(userProfileCard).getByText(/john doe/i);
const editButton = within(userProfileCard).getByRole('button', { name: /edit/i });
expect(userName).toBeVisible();
expect(editButton).toBeVisible();
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
// Example: form reads Redux or routes — use renderWithProviders (Rule 0).
// For a form with only local state and mocked submit handlers, `render` is enough.
renderWithProviders(<MyForm />);
const user = userEvent.setup();
const input = screen.getByLabelText(/name/i);
const button = screen.getByRole('button', { name: /submit/i });
// Simulate typing
await user.type(input, 'John Doe');
// Simulate clicking
await user.click(button);
Why userEvent over fireEvent:
userEvent simulates real user behavior (focus, blur, keyboard events)fireEvent dispatches raw DOM events (not realistic)userEvent catches more bugs related to event handlingawaitit('should display an error message when the API call fails', async () => {
jest.spyOn(k8sModule, 'k8sGet').mockRejectedValue(new Error('API Error'));
render(<MyComponent />);
const errorMessage = await screen.findByText(/Could not load data/i);
expect(errorMessage).toBeVisible();
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
it('should find the element', () => {
render(<MyComponent />);
// If a query fails, use debug() to see the DOM
// screen.debug();
// You can also debug a specific element
// const form = screen.getByRole('form');
// screen.debug(form);
const button = screen.getByRole('button', { name: /submit/i });
expect(button).toBeVisible();
});
Format: it('should [expected result] when [condition]')
// ✅ GOOD
it('should display an error when the API call fails')
// ❌ AVOID
it('works')
it('renders')
DO NOT use toMatchSnapshot(), toMatchInlineSnapshot(), or error snapshot matchers. Snapshot tests are brittle, give false security, and test implementation details. Prefer toStrictEqual, toMatchObject, or RTL queries on user-visible output.
Enforcement: jest/no-restricted-matchers from eslint-plugin-console errors on these matchers for paths matched by plugin:console/testing-library-tests (the same **/*spec* / **/__tests__** globs used for RTL lint).
Default: Call render() inside each it block for test isolation.
May use beforeEach only if ALL tests in the block:
Store mock data in centralized files (e.g., __mocks__/k8sResourcesMocks.ts). This:
MANDATORY: After generating tests, perform cleanup to ensure code quality and maintainability.
Remove any imports that are not used in the test file:
// ❌ BAD - Unused imports
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { k8sCreate, k8sPatch, k8sUpdate } from '@console/internal/module/k8s';
// ... but only using render, screen, userEvent
// ✅ GOOD - Only what's needed
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { k8sCreate } from '@console/internal/module/k8s';
Only mock what's actually used in tests:
// ❌ BAD - Mocking unused components
jest.mock('../ComponentA', () => () => null);
jest.mock('../ComponentB', () => () => null);
jest.mock('../ComponentC', () => () => null);
// ... but ComponentB and ComponentC are never rendered
// ✅ GOOD - Only mock what's used
jest.mock('../ComponentA', () => () => null);
Avoid testing the same behavior multiple times:
// ❌ BAD - Redundant tests
it('should render the button', () => {
render(<MyComponent />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('should display the button', () => {
render(<MyComponent />);
expect(screen.getByRole('button')).toBeVisible();
});
// ✅ GOOD - Single comprehensive test
it('should render the button', () => {
render(<MyComponent />);
expect(screen.getByRole('button', { name: 'Submit' })).toBeVisible();
});
Delete commented-out code, debugging statements, and console.logs:
// ❌ BAD - Commented code left in
it('should work', () => {
render(<MyComponent />);
// screen.debug(); // TODO: remove
// const oldTest = screen.getByTestId('old-id');
expect(screen.getByRole('button')).toBeVisible();
});
// ✅ GOOD - Clean, production-ready
it('should work', () => {
render(<MyComponent />);
expect(screen.getByRole('button')).toBeVisible();
});
Clean up any variables that are declared but never used:
// Imports omitted for brevity - see Rule 14 for full import pattern
import userEvent from '@testing-library/user-event';
// ❌ BAD - Unused variables
it('should submit form', async () => {
const mockData = { foo: 'bar' };
const unusedSpy = jest.spyOn(console, 'log');
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<Form onSubmit={onSubmit} />);
await user.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalled();
});
// ✅ GOOD - Only necessary variables
it('should submit form', async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<Form onSubmit={onSubmit} />);
await user.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalled();
});
Only add static methods to mocks if they're actually called:
// ❌ BAD - Mock has methods that are never called
jest.mock('../SelectorInput', () => Object.assign(
jest.fn(() => null),
{
objectify: jest.fn(),
arrayify: jest.fn(),
someMethodNeverUsed: jest.fn(), // ← Never called
anotherUnusedMethod: jest.fn(), // ← Never called
}
));
// ✅ GOOD - Only methods that are used
jest.mock('../SelectorInput', () => Object.assign(
jest.fn(() => null),
{
objectify: jest.fn(),
arrayify: jest.fn(),
}
));
Cleanup Checklist:
IMPORTANT: Generate between 5 and 10 focused, high-value tests per component.
Priority Order:
Critical User Flows (2-3 tests)
Error States (2-3 tests)
Conditional Rendering (2-3 tests)
User Interactions (1-2 tests)
Accessibility (1 test)
What NOT to Test (when limiting to 5-10):
❌ Multiple variations of the same behavior ❌ Testing every prop combination ❌ Minor UI variations (button text, colors) ❌ Component existence tests ❌ Trivial rendering checks
❌ BAD - Too Few Tests (3 tests):
describe('MyForm', () => {
it('should render the form');
it('should submit when valid');
it('should show error when invalid');
});
❌ BAD - Too Many Tests (15 tests):
describe('MyForm', () => {
it('should render the form');
it('should render the name input');
it('should render the email input');
it('should render the phone input');
it('should render the submit button');
it('should render the cancel button');
it('should enable submit when name is filled');
it('should enable submit when email is filled');
it('should enable submit when all fields filled');
it('should disable submit when name is empty');
it('should disable submit when email is empty');
it('should show error for invalid email');
it('should show error for invalid phone');
it('should submit when form is valid');
it('should call onCancel when cancel clicked');
});
✅ GOOD - Focused 8 Tests (within 5-10 range):
describe('MyForm', () => {
// Critical Flow (2)
it('should render all form fields and buttons');
it('should submit form with valid data');
// Error States (3)
it('should show validation errors for invalid email');
it('should display error message when submission fails');
it('should disable submit button when required fields empty');
// Conditional Rendering (2)
it('should show loading state during submission');
it('should populate form fields when editing existing data');
// Accessibility (1)
it('should have accessible form labels and buttons');
});
✅ ALSO GOOD - Minimal 5 Tests (for simple components):
describe('SimpleButton', () => {
// Critical Flow (2)
it('should render button with correct label');
it('should call onClick when clicked');
// Error States (1)
it('should be disabled when disabled prop is true');
// Conditional Rendering (1)
it('should show loading spinner when loading');
// Accessibility (1)
it('should have accessible button role and label');
});
If a component is complex enough to need more than 10 tests, it's a sign the component should be split:
// Instead of 20 tests for one large component:
describe('ComplexDashboard', () => {
// 20 tests...
});
// Split into smaller components with focused tests:
describe('DashboardHeader', () => {
// 5 tests
});
describe('DashboardFilters', () => {
// 5 tests
});
describe('DashboardDataGrid', () => {
// 7 tests
});
describe('DashboardActions', () => {
// 3 tests
});
5-10 Tests Rule Enforcement:
CRITICAL: All tests MUST have ZERO act() warnings. This rule is strictly enforced.
Warning: An update to ComponentName inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
Strategy 1: Use userEvent with async/await
// ❌ BAD: Not awaiting user interactions
const user = userEvent.setup();
user.click(button); // Missing await
expect(screen.getByText('Updated')).toBeInTheDocument();
// ✅ GOOD: Await userEvent interactions
const user = userEvent.setup();
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
Strategy 2: Use findBy queries (preferred for new elements)*
// ❌ BAD: Using getBy for async content
const user = userEvent.setup();
await user.click(button);
expect(screen.getByText('Loaded')).toBeInTheDocument(); // May fail if async
// ✅ GOOD: Use findBy* which waits automatically
const user = userEvent.setup();
await user.click(button);
expect(await screen.findByText('Loaded')).toBeInTheDocument();
Strategy 3: Use waitFor for complex interactions (e.g., dropdowns)
// ❌ BAD: Not waiting for dropdown to open
const user = userEvent.setup();
const dropdown = screen.getByText('Select Option');
await user.click(dropdown);
// Dropdown may not be open yet
// ✅ GOOD: Wait for dropdown content to appear
const user = userEvent.setup();
const dropdown = screen.getByText('Select Option');
await user.click(dropdown);
const option = await screen.findByText('Option 1');
await user.click(option);
Note: Do NOT wrap userEvent calls in act(). Since userEvent v14+, all interactions are already wrapped in act() internally. If you see act() warnings, the cause is typically a missing await or async state update that needs waitFor/findBy*.
Strategy 4: Mock timers or async operations
// ❌ BAD: Causes act() warning from useEffect
render(<ComponentWithEffect />);
// ✅ GOOD: Wait for effects to complete
render(<ComponentWithEffect />);
await waitFor(() => {
expect(screen.getByText('Effect completed')).toBeInTheDocument();
});
Dropdown/Select interactions - PatternFly Select/Dropdown components
findBy* to wait for dropdown options to appear after clickAsync state updates - useEffect, setTimeout, promises
findBy* or waitForForm submissions - Forms that trigger async actions
waitFor to check for expected outcomeComponent cleanup - Effects running after test completes
waitFor or mock timersMissing await on userEvent calls
await userEvent interactions (e.g., await user.click())Check for act() warnings:
yarn test -- ComponentName.spec.tsx --no-coverage 2>&1 | grep -i "act()"
Expected result: No output (zero matches)
Before completing test generation:
If ANY act() warnings exist → IMMEDIATELY FIX before completing
CRITICAL: Using expect.anything() defeats the purpose of testing. Always use specific, meaningful assertions.
// ❌ BAD: expect.anything() provides no value
expect(StorageClassDropdown).toHaveBeenCalledWith(
expect.objectContaining({
id: 'storageclass-dropdown',
name: 'storageClass',
}),
expect.anything(), // ❌ FORBIDDEN
);
// ✅ GOOD: Specific assertion or omit parameter
expect(StorageClassDropdown).toHaveBeenCalledWith(
expect.objectContaining({
id: 'storageclass-dropdown',
name: 'storageClass',
}),
{}, // Specific value
);
// ❌ BAD: expect.anything() in object matching
expect(mockFn).toHaveBeenCalledWith({
foo: 'bar',
baz: expect.anything(), // ❌ FORBIDDEN
});
// ✅ GOOD: Specific value or use objectContaining without it
expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
foo: 'bar',
// Only test what matters
}),
);
// ❌ BAD: expect.anything() for return values
const result = someFunction();
expect(result).toBe(expect.anything()); // ❌ FORBIDDEN
// ✅ GOOD: Specific assertion
const result = someFunction();
expect(result).toBe('expected-value');
expect(result).toBeDefined();
expect(result).toHaveProperty('key', 'value');
If you're tempted to use expect.anything(), consider these alternatives:
Use expect.objectContaining() without the field
// Only test fields that matter
expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
importantField: 'value',
// Omit unimportant fields
}),
);
Use specific type matchers
expect(mockFn).toHaveBeenCalledWith(expect.any(String));
expect(mockFn).toHaveBeenCalledWith(expect.any(Function));
expect(mockFn).toHaveBeenCalledWith(expect.any(Object));
Use custom matchers
expect(mockFn).toHaveBeenCalledWith(
expect.stringContaining('partial'),
);
expect(mockFn).toHaveBeenCalledWith(
expect.arrayContaining(['item']),
);
Don't assert on it at all
// If a parameter doesn't matter, don't test it
expect(mockFn).toHaveBeenCalled();
// Instead of: expect(mockFn).toHaveBeenCalledWith(expect.anything())
expect.anything() in the entire test fileexpect.any(Type) when you need type checkingexpect.objectContaining() to test partial objectsValidation Command:
grep -n "expect.anything()" test-file.spec.tsx
# Must return nothing
anyIMPORTANT: While TypeScript is not strictly enforced in test files, prefer specific types when available, or leave untyped.
// ✅ PREFERRED: Specific type when available
const input = screen.getByRole('textbox') as HTMLInputElement;
// ✅ ACCEPTABLE: Use any for third-party missing types
const input = screen.getByRole('textbox') as any; // When HTMLInputElement type is not available
// ✅ PREFERRED: Specific type
const mockFn = jest.fn((data: K8sResourceKind) => data);
// ✅ ACCEPTABLE: Use any when specific type is unknown
const mockFn = jest.fn((data: any) => data);
// ✅ PREFERRED: Specific array type
const items: PodDisruptionBudgetKind[] = [];
// ✅ ACCEPTABLE: Use any[] when item types vary or are unknown
const items: any[] = [];
// ✅ PREFERRED: Specific object type
const props: CreatePVCFormProps = {};
// ✅ ACCEPTABLE: Use any for dynamic props
const props: { [key: string]: any } = {};
Import actual types from the codebase
import { K8sResourceKind } from '../../module/k8s';
const resource: K8sResourceKind = { ... };
Use built-in DOM types
const input = screen.getByRole('textbox') as HTMLInputElement;
const button = screen.getByRole('button') as HTMLButtonElement;
Use jest.Mock for type safety
(k8sCreate as jest.Mock).mockResolvedValue(resource);
Use generics when appropriate
function mockComponent<T extends object>(props: T) {
return props;
}
Use Record for object types
const config: Record<string, boolean> = {};
any for third-party missing types rather than leaving untypedas jest.Mock for mocked functionsany for clarityanyAcceptable use cases:
Best practice: Add a comment explaining why any is used:
// Using any because PatternFly types don't export this interface
const mockProps: any = { ... };
// Third-party mock with complex generic types
const mockFn = jest.fn() as any;
IMPORTANT: When high-priority queries (getByRole, getByLabelText, etc.) are impossible or unrealistic, add data-test attributes to the implementation file and use getByTestId in tests.
Use data-test only when:
DO NOT use data-test when:
Step 1: Try all accessible queries first
// ✅ Try these first
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email address');
screen.getByPlaceholderText('Enter your email');
screen.getByText('Welcome back');
Step 2: If truly impossible, add data-test to implementation file
// In Component.tsx - add data-test attribute
export const MyComponent = () => (
<div>
{/* This element has no role, label, or stable text */}
<div className="custom-widget" data-test="custom-widget">
<svg>...</svg>
</div>
</div>
);
Step 3: Use getByTestId in test file
// In Component.spec.tsx
it('should render the custom widget', () => {
render(<MyComponent />);
const widget = screen.getByTestId('custom-widget');
expect(widget).toBeVisible();
});
Use kebab-case and be descriptive:
data-test="user-profile-card"data-test="deployment-status-icon"data-test="pod-list-table"data-test="div1" (not descriptive)data-test="UserProfileCard" (not kebab-case)❌ BAD - Unnecessary data-test usage:
// Component has a button with text - use getByRole instead
<button data-test="submit-button">Submit</button>
// Test - don't do this
const button = screen.getByTestId('submit-button'); // ❌
// Test - do this instead
const button = screen.getByRole('button', { name: 'Submit' }); // ✅
✅ GOOD - Legitimate data-test usage:
// Component has icon with no accessible attributes
<span className="status-icon" data-test="deployment-status-icon">
<StatusIcon status={status} />
</span>
// Test - this is acceptable
const icon = screen.getByTestId('deployment-status-icon');
expect(icon).toHaveClass('status-icon');
✅ GOOD - Multiple identical elements:
// Component renders list of similar items
<div data-test="pod-list">
{pods.map(pod => (
<div key={pod.id} data-test={`pod-item-${pod.id}`}>
<PodIcon />
</div>
))}
</div>
// Test - query by specific pod
const podItem = screen.getByTestId('pod-item-abc123');
expect(podItem).toBeVisible();
Before adding data-test:
getByRole with accessible namegetByLabelTextgetByPlaceholderTextgetByText with partial matchingdata-test valuegetByTestId is necessaryRemember: Every data-test attribute is a missed opportunity for accessibility. Only use as a last resort.
If no component path is provided as an argument, follow this intelligent detection workflow:
Run git diff to find changed files:
git diff --name-only HEAD
Filter for React components:
*.tsx, *.jsx*.spec.tsx, *.spec.jsx, *.test.tsx, *.test.jsx (test files)*.types.ts, *.types.tsx (type definition files)__mocks__/, __tests__/ directories*utils*.ts, *utils*.tsx, *helpers*.ts (utility files)*constants*.ts, *types*.ts (non-component files)Validate components:
export const ComponentName: React.FCexport default function ComponentNameexport function ComponentNameclass ComponentName extends React.ComponentPresent detected components to user:
[1] packages/console-app/src/components/MyComponent.tsxFallback if no components detected:
@ for file autocompleteExample interaction:
No arguments provided. Checking git diff for component changes...
Found 3 React components modified:
[1] packages/console-app/src/components/forms/CreatePVCForm.tsx
[2] packages/console-shared/src/components/dashboard/UtilizationCard.tsx
[3] public/components/modals/DeleteModal.tsx
Which component would you like to generate tests for? (Enter number or 'all')
When generating tests for React components:
Analyze the component to understand its user-facing behavior
Identify test scenarios covering:
Prioritize test scenarios - Generate between 5-10 tests based on component complexity (Rule 22)
Distribution guideline:
Generate tests following all 26 rules above
Use appropriate queries based on the priority hierarchy (Rule 26 for data-test)
Use verifyInputField for text input fields - strictly enforce Rule 9
Write meaningful assertions that validate user experience
Include proper TypeScript types for type safety
Avoid testing implementation details - focus on behavior
Generate 5-10 tests based on component complexity (Rule 22)
Ensure ZERO act() warnings - strictly enforce Rule 23
BEFORE GENERATING ANY TEST:
import statementsrequire() calls ANYWHERE in the file - NO EXCEPTIONSjest.Mock when calling .mockResolvedValue() etc.verifyInputField for form components - Rule 9 strictly enforcedCode Generation Pattern:
// ✅ ALWAYS - ES6 imports at file top
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { k8sCreate } from '@console/internal/module/k8s';
import { history } from '@console/internal/components/utils';
import { verifyInputField } from '@console/shared/src/test-utils/unit-test-utils'; // For form components
// ✅ ALWAYS - Simple mocks without require()
jest.mock('../Component', () => () => null);
jest.mock('../ButtonBar', () => ({ children }) => children);
// ✅ ALWAYS - Use imported modules in tests
it('should work', () => {
(k8sCreate as jest.Mock).mockResolvedValue({});
jest.spyOn(history, 'push');
});
// ✅ ALWAYS - Use verifyInputField for form fields (Rule 9)
it('should render form field correctly', async () => {
render(<MyFormComponent />);
await verifyInputField({
inputLabel: 'Name',
containerId: 'name-field',
initialValue: '',
testValue: 'my-resource',
isRequired: true,
});
});
// ❌ NEVER - require() in test body
it('should work', () => {
const { k8sCreate } = require('@console/internal/module/k8s'); // ❌ FORBIDDEN
});
// ❌ NEVER - require() in mock factory
jest.mock('../Component', () => {
const React = require('react'); // ❌ FORBIDDEN ANYWHERE
return () => React.createElement('div');
});
// ❌ NEVER - Manual assertions for form fields
it('should render Name field', () => {
render(<MyFormComponent />);
expect(screen.getByLabelText('Name')).toBeInTheDocument(); // ❌ Use verifyInputField instead
});
IMPORTANT: Follow this fully automated workflow with mandatory test execution:
ComponentName.spec.tsx (or .spec.ts for non-JSX files)__tests__/ directory within component directoryCreating MyComponent.spec.tsx...)BEFORE running tests, verify:
importrequire() calls ANYWHERE in the fileValidation Command:
# Check for require() - result MUST be empty or ONLY jest.requireActual
grep -n "require(" path/to/test.spec.tsx
Expected result:
# ONLY acceptable pattern (if needed for partial mocks):
12: ...jest.requireActual('@console/internal/module/k8s'),
# Everything else is FORBIDDEN
If ANY require() found that is NOT jest.requireActual → IMMEDIATELY FIX before proceeding.
Follow this step-by-step workflow and track progress internally:
yarn test -- ComponentName.spec.tsx --no-coverage
require() found → Replace with ES6 imports or simple mocksas jest.Mock)verifyInputField for form fields - See Rule 9Test Suites: 1 passed, 1 total
Tests: X passed, X total
waitForfindBy* queries for async elementsawait waitFor() after dropdown/select interactionsyarn build 2>&1 | grep -A 5 "test-file-name.spec.tsx"
verifyInputField (Rule 9 - strictly enforced)require() anywhere (except jest.requireActual for partial mocks)After all tests pass, perform cleanup following Rule 21:
# Check for unused imports (use your IDE or manual review)
# Remove any imports not referenced in the file
Examples:
within if not using scoped querieswaitFor if only using findBy*// Check each jest.mock() - is the mocked module actually used?
// Remove mocks for components that are never rendered
// ❌ Remove before finalizing
// screen.debug();
// console.log('test data:', data);
Cleanup Commands:
# Check for commented code
grep -n "// screen.debug\|// console\|// TODO" test-file.spec.tsx
# Check for unused imports (TypeScript will warn)
# Check IDE/editor warnings
Cleanup Verification:
ALL of the following must be true (corresponds to Step 1-5 workflow):
require() in the file (except jest.requireActual for partial mocks)import statementsverifyInputField for all text input fields (Rule 9 - strictly enforced)Final Validation Commands:
# 1. Verify ZERO require() violations
grep "require(" test-file.spec.tsx | grep -v "jest.requireActual"
# Must return nothing
# 2. Verify ZERO act() warnings (Rule 23)
test -- test-file.spec.tsx --no-coverage 2>&1 | grep -i "act()"
# Must return nothing (or only the test name that contains "act" if any)
# 3. Verify ZERO expect.anything() usage (Rule 24)
grep -n "expect.anything()" test-file.spec.tsx
# Must return nothing
# 4. Verify verifyInputField usage for form components (Rule 9)
# If component has text input fields, verify the utility is imported and used
grep -q "verifyInputField" test-file.spec.tsx && echo "✅ verifyInputField imported" || echo "⚠️ Check if form fields should use verifyInputField"
# 5. Verify no debugging code
grep -n "screen.debug()\|console.log\|console.debug" test-file.spec.tsx
# Must return nothing
# 6. Verify test count is 5-10 (Rule 22)
grep -c "^\s*it('.*)" test-file.spec.tsx
# Must return between 5 and 10
# 7. Verify yarn build passes for test file (Rule 21 + Step 5)
yarn build 2>&1 | grep -A 5 "test-file.spec.tsx"
# Must return "No errors found in test-file.spec.tsx" OR no output
# Check for unused imports (React, etc.), unused variables, TypeScript errors
If ANY validation fails → FIX IMMEDIATELY before completing
Note on Test Count:
Remember: "The more your tests resemble the way your software is used, the more confidence they can give you."
Generate comprehensive, well-structured test suites that validate the component works correctly from a user's perspective.
Final Checklist Before Completion:
Test Quality:
any (Rule 25)verifyInputField utility (Rule 9)Code Quality: