Generate React Testing Library tests following OCP Console best practices
argument-hint
[path/to/Component.tsx] or use @file for autocomplete
OCP Console React Component Unit Testing Best Practices
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 file
Smart Component Detection Workflow
When invoked without arguments, the slash command follows this intelligent workflow:
Check git diff: Automatically run git diff --name-only to find modified files
Filter for components: Identify .tsx and .jsx component files (exclude test files, type files, utils)
Validate components: Ensure files contain React components (not just types or utilities)
Present options: Show user the detected components and ask which to generate tests for
Fallback: If no valid components found, prompt user for component path
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.)
Introduction & Objectives
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.
Promote user-centric testing that focuses on behavior over implementation
Provide practical, rules-based guidance for common scenarios
Improve test quality, resilience, and maintainability
Rule 0: Use renderWithProviders Only When the Component Needs Redux and/or Router
Pick 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/orReact 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 testimport { render, screen } from'@testing-library/react';
import { BadgeLabel } from'./BadgeLabel';
it('renders the label', () => {
render(<BadgeLabeltext="Ready" />);
expect(screen.getByText('Ready')).toBeVisible();
});
// Connected to store and/or routes — use the console test wrapperimport { 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(<DeploymentListRowdeployment={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.
Rule 0.1: Use userEvent (Not fireEvent)
ALWAYS use userEvent from @testing-library/user-event for user interactions.
userEvent simulates real user behavior (focus, blur, keyboard events)
fireEvent dispatches raw DOM events (not realistic)
userEvent catches more bugs related to event handling
Better async handling with await
Rule 0.2: Use screen Queries (Not Destructured)
ALWAYS use screen from RTL instead of destructuring queries from render.
// FORBIDDEN — ESLint (e.g. testing-library/prefer-screen-queries) when enabledconst { getByRole, getByText } = render(<MyComponent />);
const button = getByRole('button');
// REQUIRED — use whichever render helper matches Rule 0, then always query via screenimport { 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 neededconst button = screen.getByRole('button');
Why:
Consistent query access across all tests
Better debugging with screen.debug()
Cleaner test code
ESLint rule testing-library/prefer-screen-queries enforces this
Section 1: React Testing Library Overview
The RTL Approach
RTL 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.
Core Principles
User-Centric Testing - Test what users see and interact with. DO NOT test:
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
Test file must be in __tests__/ directory within component directory
Test file must have same name as implementation file
Use .spec.tsx extension
Rule 2: Mocking Strategies
Check for Global Mocks First
Before manually mocking, check __mocks__/ directory for existing global mocks (e.g., react-i18next, localStorage, k8sResourcesMocks). These are applied automatically.
Keep Component Mocks Simple (No JSX)
Mock functions must NOT return JSX to avoid Jest hoisting errors:
Use jest.spyOn for Granular, Test-Level Control (Preferred)
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 ...
});
Controlling Redux State
DO NOT mock the useReduxStore hook. Instead, pass initialState to renderWithProviders:
⚠️ CRITICAL: Always Use ES6 Import (Never require())
STRICTLY ENFORCED - ZERO EXCEPTIONS
🚫 NO require() ANYWHERE IN TEST FILES
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 fileimport { 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 testit('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 bodiesit('should create resource', async () => {
const { k8sCreate } = require('@console/internal/module/k8s'); // ❌ FORBIDDEN
});
// ❌ NEVER in mock factories
jest.mock('../Component', () => {
constReact = require('react'); // ❌ FORBIDDEN - even here!return() =>React.createElement('div', null, 'Mock');
});
// ❌ NEVER in beforeEachbeforeEach(() => {
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:
All module imports use ES6 import statements at file top
ZERO require() calls anywhere in the file
Mocked modules imported at top and cast to jest.Mock when needed
import { render, screen } from'@testing-library/react';
importMyComponentfrom'./MyComponent';
// Top-level describe for the componentdescribe('MyComponent', () => {
// Nested describe for specific featuresdescribe('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:
All tests wrapped in top-level describe block named after component
Use nested describe blocks for related tests
Use it() method (not test())
Each it block tests only a single state or interaction
Rule 4: Use the Correct Render Function
Same 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).
Exception: Use within() for scoped queries or when you need container for specific assertions.
Rule 6: Prioritize Accessible Queries
Query Priority (most to least preferred):
getByRole
getByLabelText
getByPlaceholderText
getByText
getByDisplayValue
getByAltText
getByTitle
getByTestId (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 present
findBy* - 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').
Rule 7: Text Matching Strategy
Exact text match - Preferred when text is in a single node
Regex without i flag - When text spans multiple wrapper nodes (avoid case-insensitive matching)
Note: Avoid case-insensitive matching based on Console UX text casing convention.
Rule 8: Assertion Guidelines
// ✅ GOOD: Tests accessible name + existenceexpect(screen.getByRole('button', { name: 'Submit' })).toBeVisible();
// ❌ AVOID: Separate queries for same elementconst 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 with
toBeInTheDocument() - For structural elements or conditional rendering verification
Anti-pattern: Avoid weak assertions like .toBeTruthy() or .toBeInTheDocument() for visible elements.
Rule 9: Use Shared verifyInputField Utility - MANDATORY for Form Fields
CRITICAL: When testing form input fields, ALWAYS use verifyInputField utility. This is strictly enforced.
When to Use verifyInputField
Use verifyInputField when your test needs to verify:
✅ Input field label exists and is associated with the input
✅ Input element renders correctly
✅ Initial/default value of the input
✅ Input can accept user input (onChange behavior)
✅ Help text appears below the field
✅ Required field indicator (*) is shown
✅ Field ID and accessibility attributes
DO NOT manually write separate assertions for each of these - use the utility instead.
Usage Examples
import { verifyInputField } from'@console/shared/src/test-utils/unit-test-utils';
// ✅ GOOD: Use verifyInputField for comprehensive field testingit('should render the Name field with label, input, and help text', async () => {
render(<MyFormComponent />);
awaitverifyInputField({
inputLabel: 'Name',
containerId: 'test-name-form',
initialValue: 'test',
testValue: 'test',
helpText: 'Unique name for the resource',
isRequired: true,
});
});
// ✅ GOOD: Test multiple fields with verifyInputFieldit('should render all form fields correctly', async () => {
render(<MyFormComponent />);
awaitverifyInputField({
inputLabel: 'Name',
containerId: 'test-name-form',
initialValue: '',
testValue: 'my-resource',
isRequired: true,
});
awaitverifyInputField({
inputLabel: 'Description',
containerId: 'test-description-form',
initialValue: '',
testValue: 'A description',
helpText: 'Optional description for this resource',
isRequired: false,
});
});
// ❌ BAD: Manual assertions for form fieldsit('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();
});
When NOT to Use verifyInputField
❌ Non-input form controls (Select, Dropdown, Checkbox, Radio)
❌ Buttons or action elements
❌ Read-only text displays
❌ Custom form components that aren't text inputs
For these cases, use standard RTL queries.
Enforcement Checklist
When testing form components:
Identify all text input fields in the component
Use verifyInputField for each text input field test
Avoid manual label/input/helpText assertions
Import verifyInputField from '@console/shared/src/test-utils/unit-test-utils'
Rule 10: Test Conditional Rendering by Asserting Both States
import userEvent from'@testing-library/user-event';
it('should show content when expanded', async () => {
render(<Collapsible />);
const user = userEvent.setup();
// 1. Assert initial hidden stateexpect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
// 2. Simulate user actionawait user.click(screen.getByRole('button', { name: 'Expand' }));
// 3. Assert final visible stateexpect(screen.getByText('Hidden content')).toBeVisible();
});
Rule 11: Handle Asynchronous Behavior
// Use findBy* to wait for an element to appearconst element = await screen.findByText('Loaded content');
expect(element).toBeVisible();
// Use waitFor for complex assertionsawaitwaitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
Avoid Explicit act(): Rarely needed. render, userEvent, findBy*, and waitFor already wrap operations in act().
Rule 12: Use Lifecycle Hooks for Setup and Cleanup
import { render, screen, within } from'@testing-library/react';
render(<MyDashboard />);
const userProfileCard = screen.getByTestId('profile-card');
// Scope queries to only that cardconst userName = within(userProfileCard).getByText(/john doe/i);
const editButton = within(userProfileCard).getByRole('button', { name: /edit/i });
expect(userName).toBeVisible();
expect(editButton).toBeVisible();
Rule 14: Simulate User Events with userEvent
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 typingawait user.type(input, 'John Doe');
// Simulate clickingawait 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 handling
Better async handling with await
Rule 15: Test "Unhappy Paths" and Error States
it('should display an error message when the API call fails', async () => {
jest.spyOn(k8sModule, 'k8sGet').mockRejectedValue(newError('API Error'));
render(<MyComponent />);
const errorMessage = await screen.findByText(/Could not load data/i);
expect(errorMessage).toBeVisible();
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
Rule 16: Use screen.debug() for Help
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();
});
Rule 17: Write Descriptive Test Titles
Format:it('should [expected result] when [condition]')
// ✅ GOODit('should display an error when the API call fails')
// ❌ AVOIDit('works')
it('renders')
Rule 18: Avoid Snapshot Tests
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-consoleerrors on these matchers for paths matched by plugin:console/testing-library-tests (the same **/*spec* / **/__tests__** globs used for RTL lint).
Rule 19: Render in Each Test by Default
Default: Call render() inside each it block for test isolation.
May use beforeEach only if ALL tests in the block:
Are simple, synchronous tests
Use the exact same props and initial state
Only test different aspects of a single, unchanged render
Rule 20: Use Centralized Test Data
Store mock data in centralized files (e.g., __mocks__/k8sResourcesMocks.ts). This:
Mirrors production data structures
Makes tests more representative
Easier to maintain
Catches type-related errors early
Rule 21: Clean Up Unused Imports, Code, and Redundant Mocks
MANDATORY: After generating tests, perform cleanup to ensure code quality and maintainability.
Clean Up Unused Imports
Remove any imports that are not used in the test file:
// ❌ BAD - Unused importsimport { 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 neededimport { render, screen } from'@testing-library/react';
import userEvent from'@testing-library/user-event';
import { k8sCreate } from'@console/internal/module/k8s';
Remove Redundant Mocks
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);
Remove Duplicate or Redundant Tests
Avoid testing the same behavior multiple times:
// ❌ BAD - Redundant testsit('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 testit('should render the button', () => {
render(<MyComponent />);
expect(screen.getByRole('button', { name: 'Submit' })).toBeVisible();
});
Remove Commented Code
Delete commented-out code, debugging statements, and console.logs:
Clean up any variables that are declared but never used:
// Imports omitted for brevity - see Rule 14 for full import patternimport userEvent from'@testing-library/user-event';
// ❌ BAD - Unused variablesit('should submit form', async () => {
const mockData = { foo: 'bar' };
const unusedSpy = jest.spyOn(console, 'log');
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<FormonSubmit={onSubmit} />);
await user.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalled();
});
// ✅ GOOD - Only necessary variablesit('should submit form', async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
render(<FormonSubmit={onSubmit} />);
await user.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalled();
});
Remove Unnecessary Mock Static Methods
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 calledanotherUnusedMethod: 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:
All imports are used in the test file
All mocked modules are referenced in tests
No duplicate test cases
No commented-out code (unless needed for documentation)
No unused variables, constants, or spies
Mock static methods are only those actually called
No console.log, console.debug, or screen.debug() in final tests
Rule 22: Generate Between 5-10 Tests Per Component
IMPORTANT: Generate between 5 and 10 focused, high-value tests per component.
Why 5-10 Tests?
Minimum 5: Ensures adequate coverage of critical functionality
Maximum 10: Prevents over-testing and maintains quality focus
Quality over Quantity - Forces focus on most important behaviors
Maintainability - Easier to read, understand, and maintain
Faster Test Runs - Reduced execution time
Better Code Reviews - Reviewers can thoroughly examine each test
Reduced Redundancy - Prevents testing the same thing multiple ways
How to Choose 5-10 Tests
Priority Order:
Critical User Flows (2-3 tests)
Primary user actions (e.g., form submission, data creation)
Most important happy path scenarios
Error States (2-3 tests)
API failures, validation errors
Edge cases that break functionality
"Unhappy paths" users might encounter
Conditional Rendering (2-3 tests)
Different states/modes of the component
Loading states, empty states
Permission-based rendering
User Interactions (1-2 tests)
Click handlers, input changes
Form validation
Navigation/routing
Accessibility (1 test)
Key accessible queries work
ARIA attributes present
Keyboard navigation (if complex)
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
Examples
❌ 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');
});
When a Component Needs More Than 10 Tests
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:
Total test count is between 5-10 per component
Minimum 5 tests for adequate coverage
Maximum 10 tests to maintain quality focus
Each test covers unique, valuable behavior
Tests prioritize critical user flows and error states
No redundant or trivial tests included
Rule 23: Zero act() Warnings - Strictly Enforced
CRITICAL: All tests MUST have ZERO act() warnings. This rule is strictly enforced.
What is an act() Warning?
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 */
});
How to Fix act() Warnings
Strategy 1: Use userEvent with async/await
// ❌ BAD: Not awaiting user interactionsconst user = userEvent.setup();
user.click(button); // Missing awaitexpect(screen.getByText('Updated')).toBeInTheDocument();
// ✅ GOOD: Await userEvent interactionsconst user = userEvent.setup();
await user.click(button);
awaitwaitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
Strategy 2: Use findBy queries (preferred for new elements)*
// ❌ BAD: Using getBy for async contentconst user = userEvent.setup();
await user.click(button);
expect(screen.getByText('Loaded')).toBeInTheDocument(); // May fail if async// ✅ GOOD: Use findBy* which waits automaticallyconst 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 openconst 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 appearconst 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 useEffectrender(<ComponentWithEffect />);
// ✅ GOOD: Wait for effects to completerender(<ComponentWithEffect />);
awaitwaitFor(() => {
expect(screen.getByText('Effect completed')).toBeInTheDocument();
});
yarn test -- ComponentName.spec.tsx --no-coverage 2>&1 | grep -i "act()"
Expected result: No output (zero matches)
Enforcement Checklist
Before completing test generation:
Run tests and capture full output
Check for "not wrapped in act" warnings
Fix ALL act() warnings using strategies above
Re-run tests to verify zero warnings
Tests must pass with ZERO act() warnings
If ANY act() warnings exist → IMMEDIATELY FIX before completing
Rule 24: Never Use expect.anything() - Strictly Enforced
CRITICAL: Using expect.anything() defeats the purpose of testing. Always use specific, meaningful assertions.
Why expect.anything() is Forbidden
❌ Provides no value - test passes regardless of actual value
❌ Masks bugs - incorrect values will pass
❌ Reduces confidence - doesn't validate behavior
❌ Makes tests meaningless
Examples
// ❌ BAD: expect.anything() provides no valueexpect(StorageClassDropdown).toHaveBeenCalledWith(
expect.objectContaining({
id: 'storageclass-dropdown',
name: 'storageClass',
}),
expect.anything(), // ❌ FORBIDDEN
);
// ✅ GOOD: Specific assertion or omit parameterexpect(StorageClassDropdown).toHaveBeenCalledWith(
expect.objectContaining({
id: 'storageclass-dropdown',
name: 'storageClass',
}),
{}, // Specific value
);
// ❌ BAD: expect.anything() in object matchingexpect(mockFn).toHaveBeenCalledWith({
foo: 'bar',
baz: expect.anything(), // ❌ FORBIDDEN
});
// ✅ GOOD: Specific value or use objectContaining without itexpect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
foo: 'bar',
// Only test what matters
}),
);
// ❌ BAD: expect.anything() for return valuesconst result = someFunction();
expect(result).toBe(expect.anything()); // ❌ FORBIDDEN// ✅ GOOD: Specific assertionconst result = someFunction();
expect(result).toBe('expected-value');
expect(result).toBeDefined();
expect(result).toHaveProperty('key', 'value');
When You Think You Need expect.anything()
If you're tempted to use expect.anything(), consider these alternatives:
Use expect.objectContaining() without the field
// Only test fields that matterexpect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
importantField: 'value',
// Omit unimportant fields
}),
);
// If a parameter doesn't matter, don't test itexpect(mockFn).toHaveBeenCalled();
// Instead of: expect(mockFn).toHaveBeenCalledWith(expect.anything())
Enforcement
✅ All assertions must be specific and meaningful
❌ Zero expect.anything() in the entire test file
✅ Use expect.any(Type) when you need type checking
✅ Use expect.objectContaining() to test partial objects
Validation Command:
grep -n "expect.anything()" test-file.spec.tsx
# Must return nothing
Rule 25: Prefer Specific Types Over any
IMPORTANT: While TypeScript is not strictly enforced in test files, prefer specific types when available, or leave untyped.
Why Specific Types Are Preferred
✅ Better IDE autocomplete and IntelliSense
✅ Catches bugs at development time
✅ Makes refactoring safer
✅ Documents expected data shapes
✅ Self-documenting code
Examples
// ✅ PREFERRED: Specific type when availableconst input = screen.getByRole('textbox') asHTMLInputElement;
// ✅ ACCEPTABLE: Use any for third-party missing typesconst input = screen.getByRole('textbox') asany; // When HTMLInputElement type is not available// ✅ PREFERRED: Specific typeconst mockFn = jest.fn((data: K8sResourceKind) => data);
// ✅ ACCEPTABLE: Use any when specific type is unknownconst mockFn = jest.fn((data: any) => data);
// ✅ PREFERRED: Specific array typeconstitems: PodDisruptionBudgetKind[] = [];
// ✅ ACCEPTABLE: Use any[] when item types vary or are unknownconstitems: any[] = [];
// ✅ PREFERRED: Specific object typeconstprops: CreatePVCFormProps = {};
// ✅ ACCEPTABLE: Use any for dynamic propsconstprops: { [key: string]: any } = {};
(k8sCreate as jest.Mock).mockResolvedValue(resource);
Use generics when appropriate
function mockComponent<T extendsobject>(props: T) {
return props;
}
Use Record for object types
constconfig: Record<string, boolean> = {};
Guidelines
✅ Prefer specific types when they're readily available
✅ Useany for third-party missing types rather than leaving untyped
✅ Import types from codebase when possible
✅ Useas jest.Mock for mocked functions
✅ Document with comments when using any for clarity
When to Use any
Acceptable use cases:
Third-party library types are missing or broken
Complex mock objects where specific typing is impractical
Dynamic data structures in test fixtures
Temporary workarounds for type issues
Best practice: Add a comment explaining why any is used:
// Using any because PatternFly types don't export this interfaceconstmockProps: any = { ... };
// Third-party mock with complex generic typesconst mockFn = jest.fn() asany;
Rule 26: Add data-test Attributes as Last Resort
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.
When to Add data-test Attributes
Use data-test only when:
✅ Element has no semantic role
✅ Element has no accessible label or text
✅ Multiple identical elements exist and cannot be distinguished
✅ Element is dynamically generated with no predictable content
✅ Using text queries would be too brittle (frequently changing text)
DO NOT use data-test when:
❌ Element has a semantic role (button, textbox, checkbox, etc.)
❌ Element has accessible label or placeholder text
❌ Element has visible text that can be queried
❌ You're just being lazy - try harder to find accessible queries first
Implementation Pattern
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 attributeexportconstMyComponent = () => (
<div>
{/* This element has no role, label, or stable text */}
<divclassName="custom-widget"data-test="custom-widget"><svg>...</svg></div></div>
);
Step 3: Use getByTestId in test file
// In Component.spec.tsxit('should render the custom widget', () => {
render(<MyComponent />);
const widget = screen.getByTestId('custom-widget');
expect(widget).toBeVisible();
});
Naming Convention for data-test
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)
Examples
❌ 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 thisconst button = screen.getByTestId('submit-button'); // ❌// Test - do this insteadconst 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">
<StatusIconstatus={status} />
</span>
// Test - this is acceptableconst 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 => (
<divkey={pod.id}data-test={`pod-item-${pod.id}`}><PodIcon /></div>
))}
</div>
// Test - query by specific podconst podItem = screen.getByTestId('pod-item-abc123');
expect(podItem).toBeVisible();
Enforcement Checklist
Before adding data-test:
Attempted getByRole with accessible name
Attempted getByLabelText
Attempted getByPlaceholderText
Attempted getByText with partial matching
Confirmed element truly has no accessible query option
Added descriptive kebab-case data-test value
Documented in test comments why getByTestId is necessary
Remember: Every data-test attribute is a missed opportunity for accessibility. Only use as a last resort.
Instructions for Test Generation
Step 0: Detect Component to Test (When No Argument Provided)
If no component path is provided as an argument, follow this intelligent detection workflow:
Inform user: "No React components found in git diff"
Ask user to provide component path manually
Suggest using @ for file autocomplete
Example 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')
Step 1: Analyze the Component
When generating tests for React components:
Analyze the component to understand its user-facing behavior
Identify test scenarios covering:
Initial render state
User interactions (clicks, input, etc.)
Conditional rendering
Async operations
Edge cases and error states
Prioritize test scenarios - Generate between 5-10 tests based on component complexity (Rule 22)
Simple components (5-6 tests): Basic rendering, interactions, and accessibility
Medium components (7-8 tests): Add conditional rendering and error states
Complex components (9-10 tests): Full coverage including async operations
Distribution guideline:
2-3 Critical User Flows
2-3 Error States
1-3 Conditional Rendering
1-2 User Interactions
1 Accessibility
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)
Minimum 5 tests for adequate coverage
Maximum 10 tests for quality focus
Ensure ZERO act() warnings - strictly enforce Rule 23
⚠️ CRITICAL ENFORCEMENT: ES6 Imports Only - ZERO TOLERANCE
🚫 ABSOLUTE RULE: NO require() ANYWHERE
BEFORE GENERATING ANY TEST:
✅ Import ALL dependencies at the top using ES6 import statements
✅ Import mocked modules (k8s, history, etc.) to use in test bodies
🚫 ZERO require() calls ANYWHERE in the file - NO EXCEPTIONS
✅ Step 5 Complete: yarn build passes with no errors for test file
✅ Zero warnings in test output
✅ Clean console output (no errors, warnings, or deprecation notices)
✅ Test count is 5-10 (Rule 22 - minimum 5 for adequate coverage, maximum 10 for quality focus)
Code Quality (ES6 & Mocking)
🚫 ZERO require() in the file (except jest.requireActual for partial mocks)
✅ All mocks use simple return values (null, strings, children)
✅ NO React.createElement in any mock
✅ All imports are ES6 import statements
✅ Use verifyInputField for all text input fields (Rule 9 - strictly enforced)
Code Cleanliness (Rule 21)
✅ No unused imports - every import is referenced
✅ No unused mocks - every jest.mock() is for components actually used
✅ No duplicate tests - each test covers unique behavior
✅ No commented code - no debugging code, screen.debug(), console.log()
✅ No unused variables - all declared variables are used
✅ Clean mock methods - only static methods that are called
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:
If test count < 5: Add more tests to cover critical functionality
Ensure critical user flows are tested
Add error state coverage
Include accessibility tests
If test count > 10: Review and prioritize the most valuable tests
Remove redundant or low-value tests
Keep only the most important 10 tests
Consider splitting large components into smaller ones
Ideal range: 5-10 tests based on component complexity
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:
✅ Git diff detection implemented (when no args provided)
✅ Tests EXECUTED and validated to pass (MANDATORY)
✅ Iterative fixing applied until 100% pass rate
Test Quality:
✅ All 26 rules followed (especially Rule 9, Rule 22, Rule 23, Rule 24, Rule 26)
✅ Tests EXECUTED at least once (MANDATORY - not optional)
✅ All tests pass (100% pass rate - MANDATORY)
✅ No console warnings or errors
✅ ZERO act() warnings (Rule 23)
✅ No expect.anything() usage (Rule 24)
✅ Prefer specific types over any (Rule 25)
✅ Use data-test only as last resort (Rule 26)
✅ Form fields use verifyInputField utility (Rule 9)
✅ Test count is 5-10 (Rule 22 - minimum 5, maximum 10)