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(
- One
- Two
- 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();
});
});
});