From 96200935be3a97f5058dc36ad6cde4adc59602a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 9 Jan 2026 10:33:02 +0100 Subject: [PATCH 1/4] rename old tests --- src/__tests__/{render.test.tsx => render-old.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{render.test.tsx => render-old.test.tsx} (100%) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render-old.test.tsx similarity index 100% rename from src/__tests__/render.test.tsx rename to src/__tests__/render-old.test.tsx From 48849328d998085e9dfa5d00892c3bd4323478e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 9 Jan 2026 09:20:12 +0100 Subject: [PATCH 2/4] basic render tests . . . . . --- package.json | 2 +- src/__tests__/render.test.tsx | 240 ++++++++++++++++++++++++++++++++++ src/render.tsx | 4 +- 3 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/render.test.tsx diff --git a/package.json b/package.json index eb312d428..ab8b920c0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "clean": "del build", "test": "jest", "test:ci": "jest --maxWorkers=2", - "test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true --coverage-provider=v8", + "test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true", "test:codemods": "node scripts/test-codemods.mjs", "typecheck": "tsc", "lint": "eslint src --cache", diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx new file mode 100644 index 000000000..8d8ffd0cd --- /dev/null +++ b/src/__tests__/render.test.tsx @@ -0,0 +1,240 @@ +import * as React from 'react'; +import { Text, View } from 'react-native'; + +import { render, screen } from '..'; + +test('renders a simple component', async () => { + const TestComponent = () => ( + + Hello World + + ); + + await render(); + + expect(screen.getByTestId('container')).toBeOnTheScreen(); + expect(screen.getByText('Hello World')).toBeOnTheScreen(); +}); + +describe('render options', () => { + test('renders component with wrapper option', async () => { + const TestComponent = () => Inner Content; + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + await render(, { wrapper: Wrapper }); + + expect(screen.getByTestId('wrapper')).toBeOnTheScreen(); + expect(screen.getByTestId('inner')).toBeOnTheScreen(); + expect(screen.getByText('Inner Content')).toBeOnTheScreen(); + }); + + test('createNodeMock option is passed to renderer', async () => { + const TestComponent = () => ; + const mockNode = { testProperty: 'testValue' }; + const createNodeMock = jest.fn(() => mockNode); + + await render(, { createNodeMock }); + + expect(screen.getByTestId('test')).toBeOnTheScreen(); + }); +}); + +describe('rerender', () => { + test('rerender updates component with new props', async () => { + interface TestComponentProps { + message: string; + } + const TestComponent = ({ message }: TestComponentProps) => ( + {message} + ); + + await render(); + + expect(screen.getByText('Initial')).toBeOnTheScreen(); + + await screen.rerender(); + + expect(screen.getByText('Updated')).toBeOnTheScreen(); + expect(screen.queryByText('Initial')).not.toBeOnTheScreen(); + }); + + test('rerender works with wrapper option', async () => { + interface TestComponentProps { + value: number; + } + const TestComponent = ({ value }: TestComponentProps) => {value}; + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + await render(, { + wrapper: Wrapper, + }); + + expect(screen.getByText('1')).toBeOnTheScreen(); + + await screen.rerender(); + + expect(screen.getByText('2')).toBeOnTheScreen(); + expect(screen.getByTestId('wrapper')).toBeOnTheScreen(); + }); +}); + +test('unmount removes component from tree', async () => { + const TestComponent = () => Content; + + await render(); + + expect(screen.getByTestId('content')).toBeOnTheScreen(); + + await screen.unmount(); + + expect(screen.queryByTestId('content')).not.toBeOnTheScreen(); +}); + +describe('toJSON', () => { + test('toJSON returns null for empty children', async () => { + const TestComponent = () => null; + + await render(); + + expect(screen.toJSON()).toMatchInlineSnapshot(`null`); + }); + + test('toJSON returns single child when only one child exists', async () => { + const TestComponent = () => ( + + Single Child + + ); + + await render(); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + + + Single Child + + + `); + }); + + test('toJSON returns full tree for React fragment with multiple children', async () => { + const TestComponent = () => ( + <> + First + Second + + ); + + await render(); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + <> + + First + + + Second + + + `); + }); +}); + +describe('debug', () => { + test('debug outputs formatted component tree', async () => { + const TestComponent = () => ( + + Debug Test + + ); + + await render(); + + expect(() => { + screen.debug(); + }).not.toThrow(); + }); + + test('debug accepts options with message', async () => { + const TestComponent = () => Test; + + await render(); + + expect(() => { + screen.debug({ message: 'Custom message' }); + }).not.toThrow(); + }); +}); + +describe('result getters', () => { + test('container getter returns renderer container', async () => { + const TestComponent = () => Content; + + const result = await render(); + + expect(result.container).toMatchInlineSnapshot(` + <> + + Content + + + `); + }); + + test('root getter works correctly', async () => { + const TestComponent = () => ; + + const result = await render(); + + expect(result.root).toMatchInlineSnapshot(` + + `); + }); +}); + +describe('screen integration', () => { + test('render sets screen queries', async () => { + const TestComponent = () => ( + + First Text + Second Text + + ); + + await render(); + + expect(screen.getByTestId('text1')).toBeOnTheScreen(); + expect(screen.getByTestId('text2')).toBeOnTheScreen(); + expect(screen.getByText('First Text')).toBeOnTheScreen(); + expect(screen.getByText('Second Text')).toBeOnTheScreen(); + }); + + test('screen queries work after rerender', async () => { + interface TestComponentProps { + label: string; + } + const TestComponent = ({ label }: TestComponentProps) => {label}; + + await render(); + + expect(screen.getByText('Initial')).toBeOnTheScreen(); + + await screen.rerender(); + + expect(screen.getByText('Updated')).toBeOnTheScreen(); + }); +}); diff --git a/src/render.tsx b/src/render.tsx index aa726172e..20681a337 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -65,7 +65,8 @@ export async function render(element: React.ReactElement, options: RenderO const toJSON = (): JsonElement | null => { const json = renderer.container.toJSON(); - if (json?.children?.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (json?.children!.length === 0) { return null; } @@ -90,6 +91,7 @@ export async function render(element: React.ReactElement, options: RenderO get root(): HostElement | null { const firstChild = container.children[0]; if (typeof firstChild === 'string') { + /* istanbul ignore next */ throw new Error( 'Invariant Violation: Root element must be a host element. Detected attempt to render a string within the root element.', ); From 0ec95840b89180f12701824892a543ae433535cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 9 Jan 2026 18:25:41 +0100 Subject: [PATCH 3/4] migrate relevant older tests --- src/__tests__/render-old.test.tsx | 84 ------------------------------- src/__tests__/render.test.tsx | 53 +++++++++++++++++++ 2 files changed, 53 insertions(+), 84 deletions(-) delete mode 100644 src/__tests__/render-old.test.tsx diff --git a/src/__tests__/render-old.test.tsx b/src/__tests__/render-old.test.tsx deleted file mode 100644 index cef099f9d..000000000 --- a/src/__tests__/render-old.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from 'react'; -import { Text, View } from 'react-native'; - -import { render, screen } from '..'; - -class Banana extends React.Component { - state = { - fresh: false, - }; - - componentDidUpdate() { - if (this.props.onUpdate) { - this.props.onUpdate(); - } - } - - componentWillUnmount() { - if (this.props.onUnmount) { - this.props.onUnmount(); - } - } - - changeFresh = () => { - this.setState((state) => ({ - fresh: !state.fresh, - })); - }; - - render() { - return ( - - Is the banana fresh? - {this.state.fresh ? 'fresh' : 'not fresh'} - - ); - } -} - -test('render renders component asynchronously', async () => { - await render(); - expect(screen.getByTestId('test')).toBeOnTheScreen(); -}); - -test('render with wrapper option', async () => { - const WrapperComponent = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - await render(, { - wrapper: WrapperComponent, - }); - - expect(screen.getByTestId('wrapper')).toBeTruthy(); - expect(screen.getByTestId('inner')).toBeTruthy(); -}); - -test('rerender function updates component asynchronously', async () => { - const fn = jest.fn(); - await render(); - expect(fn).toHaveBeenCalledTimes(0); - - await screen.rerender(); - expect(fn).toHaveBeenCalledTimes(1); -}); - -test('unmount function unmounts component asynchronously', async () => { - const fn = jest.fn(); - await render(); - - await screen.unmount(); - expect(fn).toHaveBeenCalled(); -}); - -test('render accepts RCTText component', async () => { - await render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); - expect(screen.getByTestId('text')).toBeOnTheScreen(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); -}); - -test('render throws when text string is rendered without Text component', async () => { - await expect(render(Hello)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invariant Violation: Text strings must be rendered within a component. Detected attempt to render "Hello" string within a component."`, - ); -}); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 8d8ffd0cd..2d8f626b1 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -41,6 +41,20 @@ describe('render options', () => { }); }); +describe('component rendering', () => { + test('render accepts RCTText component', async () => { + await render(React.createElement('RCTText', { testID: 'text' }, 'Hello')); + expect(screen.getByTestId('text')).toBeOnTheScreen(); + expect(screen.getByText('Hello')).toBeOnTheScreen(); + }); + + test('render throws when text string is rendered without Text component', async () => { + await expect(render(Hello)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invariant Violation: Text strings must be rendered within a component. Detected attempt to render "Hello" string within a component."`, + ); + }); +}); + describe('rerender', () => { test('rerender updates component with new props', async () => { interface TestComponentProps { @@ -80,6 +94,7 @@ describe('rerender', () => { expect(screen.getByText('2')).toBeOnTheScreen(); expect(screen.getByTestId('wrapper')).toBeOnTheScreen(); }); + }); test('unmount removes component from tree', async () => { @@ -94,6 +109,44 @@ test('unmount removes component from tree', async () => { expect(screen.queryByTestId('content')).not.toBeOnTheScreen(); }); +test('rerender calls componentDidUpdate and unmount calls componentWillUnmount', async () => { + interface ClassComponentProps { + onUpdate?: () => void; + onUnmount?: () => void; + } + class ClassComponent extends React.Component { + componentDidUpdate() { + if (this.props.onUpdate) { + this.props.onUpdate(); + } + } + + componentWillUnmount() { + if (this.props.onUnmount) { + this.props.onUnmount(); + } + } + + render() { + return Class Component; + } + } + + const onUpdate = jest.fn(); + const onUnmount = jest.fn(); + await render(); + expect(onUpdate).toHaveBeenCalledTimes(0); + expect(onUnmount).toHaveBeenCalledTimes(0); + + await screen.rerender(); + expect(onUpdate).toHaveBeenCalledTimes(1); + expect(onUnmount).toHaveBeenCalledTimes(0); + + await screen.unmount(); + expect(onUpdate).toHaveBeenCalledTimes(1); + expect(onUnmount).toHaveBeenCalledTimes(1); +}); + describe('toJSON', () => { test('toJSON returns null for empty children', async () => { const TestComponent = () => null; From 0f36e58e6cae9e7659d62bd9fc7fb3a2e2eef460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 9 Jan 2026 18:28:04 +0100 Subject: [PATCH 4/4] . --- src/__tests__/render.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 2d8f626b1..ec4d9a170 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -94,7 +94,6 @@ describe('rerender', () => { expect(screen.getByText('2')).toBeOnTheScreen(); expect(screen.getByTestId('wrapper')).toBeOnTheScreen(); }); - }); test('unmount removes component from tree', async () => {