From 498ef37882263f9ab0dcd877761a48b74f91a339 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 2 Mar 2026 23:27:21 +0100 Subject: [PATCH 1/4] implement realistic hiding elements --- src/render.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/render.tsx b/src/render.tsx index cddbd75b3..3e6029133 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { StyleProp } from 'react-native'; import { createRoot, type HostElement, @@ -39,6 +40,10 @@ export async function render(element: React.ReactElement, options: RenderO const rendererOptions: RootOptions = { textComponentTypes: HOST_TEXT_NAMES, publicTextComponentTypes: ['Text'], + transformHiddenInstanceProps: ({ props }) => ({ + ...props, + style: withHiddenStyle(props.style as StyleProp), + }), }; const wrap = (element: React.ReactElement) => (Wrapper ? {element} : element); @@ -117,3 +122,13 @@ function makeDebug(renderer: Root): DebugFunction { } return debugImpl; } + +type StyleLike = Record; + +function withHiddenStyle(style: StyleProp): StyleProp { + if (style == null) { + return { display: 'none' }; + } + + return [style, { display: 'none' }]; +} From 88120d18ed89ee08d20f3fb1369a35cc0afa527e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 13 Apr 2026 23:29:07 +0200 Subject: [PATCH 2/4] test hidden instance props --- src/__tests__/render.test.tsx | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 25e745e9e..9002dc2fa 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -4,6 +4,22 @@ import { Text, View } from 'react-native'; import { render, screen } from '..'; import { _console, logger } from '../helpers/logger'; +function MaybeSuspend({ + children, + promise, + suspend, +}: { + children: React.ReactNode; + promise: Promise; + suspend: boolean; +}) { + if (suspend) { + React.use(promise); + } + + return children; +} + test('renders a simple component', async () => { const TestComponent = () => ( @@ -77,6 +93,64 @@ describe('render options', () => { }); }); +describe('hidden instance props', () => { + test('sets hidden suspended elements with no style to display none', async () => { + const promise = new Promise(() => {}); + + function TestComponent({ suspend }: { suspend: boolean }) { + return ( + Loading...}> + + + Ready + + + + ); + } + + await render(); + + expect(screen.getByText('Ready')).toBeOnTheScreen(); + + await screen.rerender(); + + expect(screen.getByText('Loading...')).toBeOnTheScreen(); + expect( + screen.getByTestId('hidden-target', { includeHiddenElements: true }).props.style, + ).toEqual({ + display: 'none', + }); + }); + + test('appends display none when suspending an element with existing style', async () => { + const promise = new Promise(() => {}); + + function TestComponent({ suspend }: { suspend: boolean }) { + return ( + Loading...}> + + + Ready + + + + ); + } + + await render(); + + expect(screen.getByText('Ready')).toBeOnTheScreen(); + + await screen.rerender(); + + expect(screen.getByText('Loading...')).toBeOnTheScreen(); + expect( + screen.getByTestId('hidden-target', { includeHiddenElements: true }).props.style, + ).toEqual([{ opacity: 0.5 }, { display: 'none' }]); + }); +}); + describe('component rendering', () => { test('render accepts RCTText component', async () => { await render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); From 7c260e13100b797cf460b3e08e65a2818f3f7317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 15 Apr 2026 17:04:40 +0200 Subject: [PATCH 3/4] test tweaks --- src/__tests__/render.test.tsx | 138 +++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 28 deletions(-) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 9002dc2fa..ec0c3a263 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -20,6 +20,24 @@ function MaybeSuspend({ return children; } +function TestSuspenseWrapper({ + children, + promise, + suspend, +}: { + children: React.ReactNode; + promise: Promise; + suspend: boolean; +}) { + return ( + Loading...}> + + {children} + + + ); +} + test('renders a simple component', async () => { const TestComponent = () => ( @@ -94,26 +112,47 @@ describe('render options', () => { }); describe('hidden instance props', () => { - test('sets hidden suspended elements with no style to display none', async () => { + test('does not retain hidden UI when the component suspends on initial render', async () => { const promise = new Promise(() => {}); - function TestComponent({ suspend }: { suspend: boolean }) { - return ( - Loading...}> - - - Ready - - - - ); - } + await render( + + + Ready + + , + ); + + expect(screen.getByText('Loading...')).toBeOnTheScreen(); + expect(screen.queryByTestId('hidden-target')).not.toBeOnTheScreen(); + expect(screen.queryByTestId('hidden-target', { includeHiddenElements: true })).toBeNull(); + expect(screen.toJSON()).toMatchInlineSnapshot(` + + Loading... + + `); + }); + + test('sets hidden suspended elements with no style to display none', async () => { + const promise = new Promise(() => {}); - await render(); + await render( + + + Ready + + , + ); expect(screen.getByText('Ready')).toBeOnTheScreen(); - await screen.rerender(); + await screen.rerender( + + + Ready + + , + ); expect(screen.getByText('Loading...')).toBeOnTheScreen(); expect( @@ -121,33 +160,76 @@ describe('hidden instance props', () => { ).toEqual({ display: 'none', }); + expect(screen.toJSON()).toMatchInlineSnapshot(` + <> + + + Ready + + + + Loading... + + + `); }); test('appends display none when suspending an element with existing style', async () => { const promise = new Promise(() => {}); - function TestComponent({ suspend }: { suspend: boolean }) { - return ( - Loading...}> - - - Ready - - - - ); - } - - await render(); + await render( + + + Ready + + , + ); expect(screen.getByText('Ready')).toBeOnTheScreen(); - await screen.rerender(); + await screen.rerender( + + + Ready + + , + ); expect(screen.getByText('Loading...')).toBeOnTheScreen(); expect( screen.getByTestId('hidden-target', { includeHiddenElements: true }).props.style, ).toEqual([{ opacity: 0.5 }, { display: 'none' }]); + expect(screen.toJSON()).toMatchInlineSnapshot(` + <> + + + Ready + + + + Loading... + + + `); }); }); From 2440978738600a8aef6cd91b28966f709eb28b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 15 Apr 2026 17:09:08 +0200 Subject: [PATCH 4/4] more tests --- src/__tests__/render.test.tsx | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index ec0c3a263..f39d3228b 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -231,6 +231,81 @@ describe('hidden instance props', () => { `); }); + + test('applies hidden styles to multiple direct child views when suspending', async () => { + const promise = new Promise(() => {}); + + await render( + + + First + + + Second + + , + ); + + expect(screen.getByText('First')).toBeOnTheScreen(); + expect(screen.getByText('Second')).toBeOnTheScreen(); + + await screen.rerender( + + + First + + + Second + + , + ); + + expect(screen.getByText('Loading...')).toBeOnTheScreen(); + expect( + screen.getByTestId('hidden-target-1', { includeHiddenElements: true }).props.style, + ).toEqual({ + display: 'none', + }); + expect( + screen.getByTestId('hidden-target-2', { includeHiddenElements: true }).props.style, + ).toEqual([{ opacity: 0.5 }, { display: 'none' }]); + expect(screen.toJSON()).toMatchInlineSnapshot(` + <> + + + First + + + + + Second + + + + Loading... + + + `); + }); }); describe('component rendering', () => {