Skip to content

testing library formula create patterns

Paulo Gomes da Cruz Junior edited this page Sep 16, 2026 · 1 revision

title: "Test Patterns — Formula Create Screen Coverage" type: pattern-guide domain: frontend framework: react testing: "vitest + @testing-library/react" component: "FormulaConfigurationCreateForm" tags: [testing-library, carbon, vitest, tanstack-form, mocking, patterns] created: 2026-09-16

Test Patterns — Formula Create Screen Coverage

Context

The formula create screen (FormulaConfigurationCreateForm) coverage improvement effort expanded test suites from 57.9% to ~93%+ across three files. This guide captures the patterns and workarounds discovered during that effort.

Pattern 1 — Carbon RadioButtonGroup Mocking in jsdom

Problem

jsdom does not fire React's synthetic onChange on native radio inputs. Carbon's RadioButton.handleOnChange chain breaks when tests use fireEvent.click or fireEvent.change on radio inputs.

Root Cause

Carbon's RadioButtonGroup manages its own internal state via event handlers that expect React's synthetic event system. jsdom's native radio input events bypass this chain.

Solution

Mock @carbon/react with a simplified RadioButtonGroup that captures the onChange prop into a test-accessible callback:

vi.mock("@carbon/react", async () => {
  const actual = await vi.importActual("@carbon/react");
  return {
    ...actual,
    RadioButtonGroup: ({ onChange, children, ...props }) => {
      mocks.radioGroupOnChange = onChange;
      return <div data-testid={props["data-testid"]}>{children}</div>;
    },
  };
});

Then call the captured callback directly in tests:

// Trigger the radio group change
mocks.radioGroupOnChange!("area-a");

// Assert the side effect
expect(mockNavigate).toHaveBeenCalled();

Why Not fireEvent

fireEvent.click and fireEvent.change on radio inputs do not trigger Carbon's internal handler chain in jsdom. The component state never updates. Direct callback invocation is the only reliable approach.

Pattern 2 — TanStack Form v1 setFieldValue Re-render Limitation

Problem

form.setFieldValue("area", value) triggers a re-render, but form.state.values.area (direct read) does not propagate the updated value to child components in the test environment.

Root Cause

TanStack Form v1's state propagation works through React's reconciliation cycle. In jsdom test environments, the direct state read may not reflect the updated value before assertions run.

Solution

Verify the side effect that proves the handler ran, rather than asserting the value directly:

// Instead of asserting the value directly
// BAD: expect(form.state.values.area).toBe("area-a");

// Verify the side effect
mockSetIsReviewing.mockImplementation((val) => {
  if (!val) {
    // This triggers a re-render that proves the handler ran
  }
});

// Assert the visible outcome
expect(screen.getByRole("button", { name: /submit/i })).toBeTruthy();

Pattern 3 — Carbon Button type="submit" with userEvent

Problem

Carbon wraps the native button element. userEvent.click on a Carbon Button with type="submit" does not trigger the parent form's onSubmit handler.

Root Cause

Carbon's button component intercepts the click event and does not propagate it to the native form submission flow in jsdom.

Solution

Use fireEvent.submit on the form element directly:

const form = screen.getByRole("form", { name: /formula/i });
fireEvent.submit(form);

Why Not userEvent.click

userEvent.click fires pointer events that Carbon intercepts. The native form submission never fires. fireEvent.submit bypasses Carbon's wrapper and triggers the form's onSubmit directly.

Pattern 4 — Multiple Identical Test IDs

Problem

When a mock component renders multiple instances (e.g., 3 FormulaSection components each with data-testid="formula-section-area"), getByTestId("formula-section-area") throws because it finds multiple elements.

Solution

Use getAllByTestId and select by index:

const sections = screen.getAllByTestId("formula-section-area");
expect(sections[0]).toBeTruthy();
expect(sections[1]).toBeTruthy();

Alternative

Add unique identifiers to distinguish instances:

// Component
<div data-testid={`formula-section-area-${index}`}>

// Test
const section0 = screen.getByTestId("formula-section-area-0");

Pattern 5 — FormulaInput Mock Pattern

Problem

Complex child components with multiple callbacks (onChange, onValidationError) need controlled simulation of user interactions.

Solution

Create a minimal mock that captures callback refs and provides trigger buttons:

vi.mock("../FormulaInput", () => ({
  default: ({ onChange, onValidationError, expressionRef }) => {
    mocks.formulaInputOnChange = onChange;
    mocks.formulaInputOnValidationError = onValidationError;
    mocks.formulaInputExpressionRef = expressionRef;
    return (
      <div data-testid="formula-input-mock">
        <button onClick={() => onChange("AREA * 2")}>Set Expression</button>
        <button onClick={() => onValidationError("Invalid expression")}>
          Trigger Error
        </button>
      </div>
    );
  },
}));

Then simulate user interactions in tests:

// Simulate user entering expression
screen.getByRole("button", { name: /set expression/i }).click();

// Assert the callback was called with correct value
expect(mockOnChange).toHaveBeenCalledWith("AREA * 2");

Benefits

  • Tests verify callback contracts without rendering complex child components
  • Mock captures all callback invocations for assertion
  • Trigger buttons simulate realistic user interactions
  • Mock stays minimal and focused on the interface contract

Related Patterns

Validation

After applying these patterns:

  • vitest run — all tests pass
  • eslint --max-warnings=0 — clean
  • tsc --noEmit — clean
  • Coverage meets target (~93%+ for formula create screen files)

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally