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:
- 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.
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:
- Run the provided codemods:
rntl-v14-update-deps and rntl-v14-async-functions
- Replace
react-test-renderer with test-renderer@1.1 (for React 19.1)
- Update or remove the
createRenderer utility
- Migrate pressed-state tests to Option 2 (pure function extraction) — cleanest long-term approach, avoids reliance on internal component tree traversal
- All
render(), renderHook(), fireEvent(), and act() calls become async — update all test files accordingly
Related
Context
PR #1165 introduced a shared
createRendererutility (src/test-utils/createRenderer.tsx) to eliminate the copy-pastedReactTestRenderer.create()+act()wrapper across 7 test files. The utility is needed because React 19 requiresReactTestRenderer.create()to be wrapped inact().The remaining usage of
createRendereris specifically for testing pressed-state background colors onPressable-based components (ButtonBase,ButtonPrimary,ButtonSecondary,ButtonTertiary,ButtonHero,ActionListItem). These tests extract thestylefunction 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
Pressablemanages itspressedstate 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 updatePressable's internalpressedstateuserEvent.press()— completes the full press cycle (pressIn → pressOut), sopressedreverts tofalsebefore any assertion can runuserEvent.pressIn()— does not exist in RNTL v13. OnlypressandlongPressare available onUserEventInstanceThe 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()totoStrictEqual(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
getContainerClassNameout of the component to a module-level exported function. Test that function directly with plain stringtoBe()assertions — nocreateRenderer, no tree traversal, notwneeded.Tradeoff: The function returns a class name string (required by
ButtonBase'stwClassNameprop). Usingtw.style()inside it would change the return type to a style object and break thetwClassNameAPI. 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:tree.root.find(). ThecreateRendererapproach of finding a node bynode.props.style === 'function'will stop working.react-test-rendererreplaced bytest-renderer— new package from @mdjastrzebski/test-renderer, pinned to React 19 minor version. ThecreateRendererutility 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.pressInwas investigated as a v14 feature but is not planned — the current main branch of RNTL still only exposespressandlongPressonUserEventInstance.Recommended path when upgrading to RNTL v14
When v14 goes stable:
rntl-v14-update-depsandrntl-v14-async-functionsreact-test-rendererwithtest-renderer@1.1(for React 19.1)createRendererutilityrender(),renderHook(),fireEvent(), andact()calls become async — update all test files accordinglyRelated
createRendererutility