Skip to content

Refactor pressed-state style tests away from createRenderer once RNTL v14 is stable #1182

@georgewrmarshall

Description

@georgewrmarshall

Context

PR #1165 introduced a shared createRenderer utility (src/test-utils/createRenderer.tsx) to eliminate the copy-pasted ReactTestRenderer.create() + act() wrapper across 7 test files. The utility is needed because React 19 requires ReactTestRenderer.create() to be wrapped in act().

The remaining usage of createRenderer is specifically for testing pressed-state background colors on Pressable-based components (ButtonBase, ButtonPrimary, ButtonSecondary, ButtonTertiary, ButtonHero, ActionListItem). These tests extract the style function prop directly from the component tree and invoke it with { pressed: true/false } to assert the correct Tailwind class is applied.

Why this pattern exists

React Native's Pressable manages its pressed state internally through the responder system. None of the standard RNTL interaction APIs can trigger it cleanly:

  • fireEvent(el, 'pressIn') — fires the event handler but does not update Pressable's internal pressed state
  • userEvent.press() — completes the full press cycle (pressIn → pressOut), so pressed reverts to false before any assertion can run
  • userEvent.pressIn()does not exist in RNTL v13. Only press and longPress are available on UserEventInstance

The only way to test both branches of a pressed style function in RNTL v13 is to extract the function from the component tree and invoke it directly.

Options explored

Option 1: Keep createRenderer (current approach)

Extract the shared helper once, use it wherever pressed-state branch coverage is required. Tests are verbose but correct. The assertions were strengthened from toBeDefined() to toStrictEqual(expect.objectContaining(tw\class`))`.

Tradeoff: Relies on React Test Renderer internals (tree.root.find, node.props.style). Will break in RNTL v14 where only host elements are exposed (composite component props no longer visible).

Option 2: Extract pressed logic to pure functions

Move getContainerClassName out of the component to a module-level exported function. Test that function directly with plain string toBe() assertions — no createRenderer, no tree traversal, no tw needed.

Tradeoff: The function returns a class name string (required by ButtonBase's twClassName prop). Using tw.style() inside it would change the return type to a style object and break the twClassName API. The string approach foregoes some Tailwind IntelliSense/linting benefits.

Option 3: Remove tests + per-file coverage threshold

Accept that pressed visual states are better verified in Storybook on device. Add explicit coverage threshold exceptions for the pressed branches.

Tradeoff: Loses automated coverage of an important visual contract.

What changes in RNTL v14

RNTL v14 (currently 14.0.0-rc.0, released 2026-05-07) makes two changes that affect this:

  1. Query results expose host elements only — composite component props are no longer accessible through tree.root.find(). The createRenderer approach of finding a node by node.props.style === 'function' will stop working.
  2. react-test-renderer replaced by test-renderer — new package from @mdjastrzebski/test-renderer, pinned to React 19 minor version. The createRenderer utility would need updating.

v14 also requires React 19 (already on 19.1.0) and RN >= 0.78 (already on 0.81.5), so the peer dep upgrade is already satisfied when v14 goes stable.

userEvent.pressIn was investigated as a v14 feature but is not planned — the current main branch of RNTL still only exposes press and longPress on UserEventInstance.

Recommended path when upgrading to RNTL v14

When v14 goes stable:

  1. Run the provided codemods: rntl-v14-update-deps and rntl-v14-async-functions
  2. Replace react-test-renderer with test-renderer@1.1 (for React 19.1)
  3. Update or remove the createRenderer utility
  4. Migrate pressed-state tests to Option 2 (pure function extraction) — cleanest long-term approach, avoids reliance on internal component tree traversal
  5. All render(), renderHook(), fireEvent(), and act() calls become async — update all test files accordingly

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions