import React from 'react'; import { act, render, waitFor } from '@testing-library/react-native'; import RenderHTML from '../RenderHTML'; import ImgTag from '../elements/IMGElement'; import { CustomBlockRenderer, CustomTextualRenderer } from '../render/render-types'; import { defaultHTMLElementModels, HTMLContentModel } from '@native-html/transient-render-engine'; import { Image, Text } from 'react-native'; import { useRendererProps } from '../context/RenderersPropsProvider'; import TNodeChildrenRenderer from '../TNodeChildrenRenderer'; import OLElement from '../elements/OLElement'; import ULElement from '../elements/ULElement'; import { HTMLElementModelRecord } from '../shared-types'; describe('RenderHTML', () => { it('should render without error when providing a source', () => { expect(() => render( Hello world

' }} debug={false} /> ) ).not.toThrow(); }); it('should render without error when missing a source', () => { //@ts-expect-error missing source expect(() => render()).not.toThrow(); }); it('should print a snapshot in debug mode when __DEV__ is true', () => { console.info = jest.fn(); render(Hello world

' }} debug={true} />); expect(console.info).toHaveBeenNthCalledWith( 1, expect.stringContaining('Transient Render Tree update') ); }); describe('regarding internal renderers', () => { it('should use internal renderer for
    elements', async () => { const { UNSAFE_getByType } = render(
  1. One
  2. Two
  3. Three
' }} debug={false} contentWidth={0} /> ); await waitFor(() => UNSAFE_getByType(OLElement)); }); it('should use internal renderer for
    elements', async () => { const { UNSAFE_getByType } = render(
  • One
  • Two
  • Three
' }} debug={false} contentWidth={0} /> ); await waitFor(() => UNSAFE_getByType(ULElement)); }); it('should update contentWidth when contentWidth prop changes', () => { const contentWidth = 300; const nextContentWidth = 200; const { UNSAFE_getByType, update } = render( ' }} debug={false} contentWidth={contentWidth} /> ); expect(UNSAFE_getByType(ImgTag)).toHaveProp('contentWidth', contentWidth); update( ' }} debug={false} contentWidth={nextContentWidth} /> ); expect(UNSAFE_getByType(ImgTag)).toHaveProp( 'contentWidth', nextContentWidth ); }); it('should merge `viewStyle` to renderer', () => { const { getByA11yRole } = render( ' }} debug={false} defaultViewProps={{ style: { backgroundColor: 'red' } }} contentWidth={200} /> ); expect(getByA11yRole('image')).toHaveStyle({ backgroundColor: 'red' }); }); it('should use internal text renderer for tags', async () => { const { findByText } = render( ' }} debug={false} contentWidth={0} /> ); await findByText('\u200B'); }); it('should render
tags to line breaks when followed by text', () => { const { queryByText } = render( Two' }} debug={false} contentWidth={0} /> ); expect(queryByText('\n')).toBeDefined(); }); it('should render
tags to line breaks when the tag closes an inline formatting context', () => { const { queryByText } = render(
' }} debug={false} contentWidth={0} /> ); expect(queryByText('\n')).toBeDefined(); }); it('should invoke renderersProps.a.onPress on press', async () => { const onPress = jest.fn(); const { findByTestId } = render( Hello world!' }} renderersProps={{ a: { onPress } }} debug={false} contentWidth={0} /> ); const anchor = await findByTestId('a'); act(() => anchor.props.onPress?.({})); expect(onPress).toHaveBeenCalled(); }); }); describe('regarding customHTMLElementsModels prop', () => { it('should support changing block content model to mixed', () => { const contentWidth = 300; const onTTreeChange = jest.fn((ttree) => expect(ttree.snapshot()).toMatchSnapshot() ); render(
Text' }} debug={false} customHTMLElementModels={{ article: defaultHTMLElementModels.article.extend({ contentModel: HTMLContentModel.mixed }) }} contentWidth={contentWidth} onTTreeChange={onTTreeChange} /> ); expect(onTTreeChange).toHaveBeenCalledTimes(1); }); }); it('should support fonts from tagsStyles specified in systemFonts', () => { const tagsStyles = { span: { fontFamily: 'Superfont' } }; const { getByTestId } = render( hi' }} debug={false} tagsStyles={tagsStyles} systemFonts={['Superfont']} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveStyle(tagsStyles.span); }); describe('regarding onTTreeChange prop', () => { const onTTreeChange = jest.fn(); render( Yuhuuu' }} debug={false} onTTreeChange={onTTreeChange} contentWidth={100} /> ); expect(onTTreeChange).toHaveBeenCalled(); }); describe('regarding onHTMLLoaded prop', () => { const onHTMLLoaded = jest.fn(); render( Yuhuuu' }} debug={false} onHTMLLoaded={onHTMLLoaded} contentWidth={100} /> ); expect(onHTMLLoaded).toHaveBeenCalled(); }); describe('regarding onDocumentMetadataLoaded prop', () => { const onDocumentMetadataLoaded = jest.fn(); render( Yuhuuu' }} debug={false} onDocumentMetadataLoaded={onDocumentMetadataLoaded} contentWidth={100} /> ); expect(onDocumentMetadataLoaded).toHaveBeenCalled(); }); describe('regarding markers', () => { it('should set `anchor` marker for `a` tags', () => { const { UNSAFE_getByType } = render( Yuhuuu' }} debug={false} contentWidth={100} /> ); const ttext = UNSAFE_getByType(Text).parent!; expect(ttext.props.tnode.markers.anchor).toBe(true); }); it('should set `edits` marker to "ins" for `ins` tags', () => { const { UNSAFE_getByType } = render( Yuhuuu' }} debug={false} contentWidth={100} /> ); const ttext = UNSAFE_getByType(Text).parent!; expect(ttext.props.tnode.markers.edits).toBe('ins'); }); it('should set `edits` marker to "del" for `del` tags', () => { const { UNSAFE_getByType } = render( Yuhuuu' }} debug={false} contentWidth={100} /> ); const ttext = UNSAFE_getByType(Text).parent!; expect(ttext.props.tnode.markers.edits).toBe('del'); }); it('should set `lang` marker for `lang` attributes', () => { const { UNSAFE_getByType } = render( Voila !

' }} debug={false} contentWidth={100} /> ); const ttext = UNSAFE_getByType(Text).parent!; expect(ttext.props.tnode.markers.lang).toBe('fr'); }); it('should set `dir` marker for `dir` attributes', () => { const { UNSAFE_getByType } = render( ٱلسَّلَامُ عَلَيْكُمْ‎

' }} debug={false} contentWidth={100} /> ); const ttext = UNSAFE_getByType(Text).parent!; expect(ttext.props.tnode.markers.direction).toBe('rtl'); }); it('should pass markers deep down in the tree', () => { const EmRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ; const { UNSAFE_getByType } = render( OneTwo' }} renderers={{ em: EmRenderer }} debug={false} contentWidth={100} /> ); const em = UNSAFE_getByType(EmRenderer); expect(em.props.tnode.markers.lang).toBe('test'); }); it('should handle setMarkersForTNode prop', () => { const { UNSAFE_getByType } = render( Two' }} debug={false} setMarkersForTNode={(targetMarkers, __parent, tnode) => { if (tnode.tagName === 'em') { //@ts-expect-error undefined marker targetMarkers.em = true; } }} contentWidth={100} /> ); const em = UNSAFE_getByType(Text).parent!; expect(em.props.tnode.markers.em).toBe(true); }); }); describe('regarding propsFromParent prop in custom renderers', () => { it('should pass propsForChildren to children', () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const EmRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ; const { UNSAFE_getByType } = render( OneTwo' }} renderers={{ span: SpanRenderer, em: EmRenderer }} debug={false} contentWidth={100} /> ); const em = UNSAFE_getByType(EmRenderer); expect(em.props.propsFromParent.test).toBe(1); }); it('should not pass propsForChildren to sub-children', () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const EmRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ; const { UNSAFE_getByType } = render( OneTwo' }} renderers={{ span: SpanRenderer, em: EmRenderer }} debug={false} contentWidth={100} /> ); const em = UNSAFE_getByType(EmRenderer); expect(em.props.propsFromParent.test).toBeUndefined(); }); it('should apply `viewProps` to TBlock renderers', () => { const DivRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ; const { getByTestId } = render( test' }} renderers={{ div: DivRenderer }} debug={false} contentWidth={100} /> ); const div = getByTestId('div'); expect(div).toHaveProp('collapsable', false); }); it('should apply `textProps` to TPhrasing renderers', () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { getByTestId } = render( foobar' }} renderers={{ span: SpanRenderer }} debug={false} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveProp('adjustsFontSizeToFit', true); }); it('should apply `textProps` to TText renderers', () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { getByTestId } = render( foo' }} renderers={{ span: SpanRenderer }} debug={false} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveProp('adjustsFontSizeToFit', true); }); it('should apply `props`', () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { getByTestId } = render( foo' }} renderers={{ span: SpanRenderer }} debug={false} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveProp('accessibilityRole', 'adjustable'); }); it('should apply `tnode.getReactNativeProps()` to TPhrasing renderers', () => { const customHTMLElementModels: HTMLElementModelRecord = { span: defaultHTMLElementModels.span.extend({ reactNativeProps: { native: { accessibilityRole: 'adjustable' } } }) }; const { getByTestId } = render( foobar
' }} customHTMLElementModels={customHTMLElementModels} debug={false} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveProp('accessibilityRole', 'adjustable'); }); it('should apply `tnode.getReactNativeProps()` to TText renderers', () => { const customHTMLElementModels: HTMLElementModelRecord = { span: defaultHTMLElementModels.span.extend({ reactNativeProps: { native: { accessibilityRole: 'adjustable' } } }) }; const { getByTestId } = render( foo' }} customHTMLElementModels={customHTMLElementModels} debug={false} contentWidth={100} /> ); const span = getByTestId('span'); expect(span).toHaveProp('accessibilityRole', 'adjustable'); }); it('should apply `tnode.getReactNativeProps()` to TBlock renderers', () => { const customHTMLElementModels: HTMLElementModelRecord = { div: defaultHTMLElementModels.span.extend({ reactNativeProps: { native: { accessibilityRole: 'adjustable' } } }) }; const { getByTestId } = render( test' }} customHTMLElementModels={customHTMLElementModels} debug={false} contentWidth={100} /> ); const div = getByTestId('div'); expect(div).toHaveProp('accessibilityRole', 'adjustable'); }); }); describe('regarding TNodeRenderer', () => { describe('TBlockRenderer', () => { it('should render a GenericPressable when provided onPress', async () => { const onPress = jest.fn(); const DivRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ; const { findByTestId } = render( ' }} debug={false} contentWidth={0} renderers={{ div: DivRenderer }} /> ); await findByTestId('generic-pressable'); }); it('should use viewProps.style', async () => { const DivRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { findByTestId } = render( ' }} debug={false} contentWidth={0} renderers={{ div: DivRenderer }} /> ); const div = await findByTestId('div'); expect(div).toHaveStyle({ marginBottom: 10, paddingBottom: 10 }); }); it('should merge viewProps.style with greater specificity than given styles', async () => { const DivRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { findByTestId } = render( ' }} debug={false} contentWidth={0} renderers={{ div: DivRenderer }} /> ); const div = await findByTestId('div'); expect(div).toHaveStyle({ marginBottom: 10 }); }); describe('TDefaultTextualRenderer', () => { it('should merge textProps.style with greater specificity than given styles', async () => { const SpanRenderer: CustomTextualRenderer = ({ TDefaultRenderer, ...props }) => ( ); const { findByTestId } = render( ' }} debug={false} contentWidth={0} renderers={{ span: SpanRenderer }} /> ); const div = await findByTestId('span'); expect(div).toHaveStyle({ marginBottom: 10 }); }); }); }); }); describe('regarding enableExperimentalMarginCollapsing prop', () => { it('should collapse margins of sibling block children when enabled', () => { const { getByTestId } = render(

text

' }} debug={false} contentWidth={100} enableExperimentalMarginCollapsing /> ); expect(getByTestId('div')).toHaveStyle({ marginBottom: 10 }); expect(getByTestId('p')).toHaveStyle({ marginTop: 0 }); }); it('should not collapse margins of sibling phrasing children when enabled', () => { const { getByTestId } = render( text' }} debug={false} contentWidth={100} enableExperimentalMarginCollapsing /> ); expect(getByTestId('div')).toHaveStyle({ marginBottom: 10 }); expect(getByTestId('span')).toHaveStyle({ marginTop: 10 }); }); it('should not collapse margins of sibling children when disabled', () => { const { getByTestId } = render(

' }} debug={false} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); expect(getByTestId('div')).toHaveStyle({ marginBottom: 10 }); expect(getByTestId('p')).toHaveStyle({ marginTop: 10 }); }); }); describe('regarding renderersProps prop', () => { it('should pass renderersProps to useRendererProps', () => { const DivRenderer = jest.fn(function DivRenderer() { expect(useRendererProps('div')).toBeDefined(); return null; }); render( ' }} debug={false} renderers={{ div: DivRenderer }} renderersProps={{ div: {} }} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); expect(DivRenderer).toHaveBeenCalledTimes(1); }); }); describe('regarding renderers prop', () => { it('should support TNodeChildrenRenderer', () => { const renderChild = jest.fn(() => null); const DivRenderer: CustomTextualRenderer = jest.fn(function DivRenderer({ TDefaultRenderer, ...props }) { return ( ); }); render( child' }} debug={false} renderers={{ div: DivRenderer }} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); expect(renderChild).toHaveBeenCalled(); }); it('should have TNodeChildrenRender support a text child', async () => { const SpanRenderer: CustomTextualRenderer = jest.fn( function SpanRenderer({ TDefaultRenderer, ...props }) { return ( ); } ); const { UNSAFE_getByType } = render( hello!' }} debug={false} renderers={{ span: SpanRenderer }} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); await waitFor(() => UNSAFE_getByType(Text)); }); it('should warn when using the default WebView component', () => { const ImageRenderer: CustomBlockRenderer = jest.fn(function SpanRenderer({ sharedProps: { WebView } }) { return ; }); console.warn = jest.fn(); render( ' }} debug={false} renderers={{ img: ImageRenderer }} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); expect(console.warn).toHaveBeenCalled(); }); }); describe('regarding TRenderEngineConfig props', () => { it('should update props when they change', async () => { const initialTagsStyles = { img: { borderBottomWidth: 2 } }; const nextTagsStyles = { img: { borderBottomWidth: 4 } }; const { update, findByTestId } = render( ' }} tagsStyles={initialTagsStyles} debug={false} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); update( ' }} tagsStyles={nextTagsStyles} debug={false} contentWidth={100} enableExperimentalMarginCollapsing={false} /> ); const img = await findByTestId('img'); expect(img).toHaveStyle({ borderBottomWidth: 4 }); }); }); describe('regarding provideEmbeddedHeaders prop', () => { it('should apply returned headers to IMG tags', async () => { const headers = { Authorization: 'Bearer XXX' }; const getSizeWithHeaders = jest.spyOn(Image, 'getSizeWithHeaders'); function provideEmbeddedHeaders(uri: string, tag: string) { expect(tag).toBe('img'); return headers; } const { UNSAFE_getByType, findByTestId } = render( ' }} debug={false} contentWidth={100} provideEmbeddedHeaders={provideEmbeddedHeaders} /> ); await findByTestId('image-success'); const image = UNSAFE_getByType(Image); expect(image.props.source.headers).toBe(headers); expect(getSizeWithHeaders).toHaveBeenCalledWith( 'https://custom.domain/', headers, expect.anything(), expect.anything() ); }); }); describe('regarding enableExperimentalBRCollapsing', () => { it('should render
tags to line breaks when followed by text', () => { const { queryByText } = render( Two' }} debug={false} contentWidth={0} enableExperimentalBRCollapsing /> ); expect(queryByText('\n')).toBeDefined(); }); it('should render
tags to empty text when the tag closes an inline formatting context', () => { const { queryByText } = render(
' }} debug={false} contentWidth={0} enableExperimentalBRCollapsing /> ); expect(queryByText('\n')).toBeNull(); }); }); });