Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/render/src/__tests__/component.render-html.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -893,4 +893,142 @@ describe('RenderHTML', () => {
expect(queryByText('\n', { normalizer: (s) => s })).toBeNull();
});
});
describe('regarding key generation in renderChildren', () => {
it('should generate unique keys for sibling elements with the same tag name', () => {
const capturedKeys: string[] = [];
const renderChild = jest.fn(({ key }) => {
capturedKeys.push(key);
return null;
});
const DivRenderer: CustomTextualRenderer = jest.fn(function DivRenderer({
TDefaultRenderer,
...props
}) {
return (
<TDefaultRenderer {...props}>
<TNodeChildrenRenderer
renderChild={renderChild}
tnode={props.tnode}
/>
</TDefaultRenderer>
);
});
render(
<RenderHTML
source={{
html: '<div><p>One</p><p>Two</p></div>'
}}
debug={false}
renderers={{ div: DivRenderer }}
contentWidth={100}
/>
);
expect(capturedKeys.length).toBeGreaterThanOrEqual(2);
expect(new Set(capturedKeys).size).toBe(capturedKeys.length);
});
it('should generate unique keys for elements with the same tag and same nodeIndex in different subtrees', () => {
const capturedKeys: string[] = [];
const renderChild = jest.fn(({ key }) => {
capturedKeys.push(key);
return null;
});
const DivRenderer: CustomTextualRenderer = jest.fn(function DivRenderer({
TDefaultRenderer,
...props
}) {
return (
<TDefaultRenderer {...props}>
<TNodeChildrenRenderer
renderChild={renderChild}
tnode={props.tnode}
/>
</TDefaultRenderer>
);
});
render(
<RenderHTML
source={{
html: '<div><div><p>Nested One</p></div><div><p>Nested Two</p></div></div>'
}}
debug={false}
renderers={{ div: DivRenderer }}
contentWidth={100}
/>
);
expect(new Set(capturedKeys).size).toBe(capturedKeys.length);
});
it('should generate a key matching the exact expected string including parent path', () => {
const capturedKeys: string[] = [];
const renderChild = jest.fn(({ key }) => {
capturedKeys.push(key);
return null;
});
const DivRenderer: CustomTextualRenderer = jest.fn(function DivRenderer({
TDefaultRenderer,
...props
}) {
return (
<TDefaultRenderer {...props}>
<TNodeChildrenRenderer
renderChild={renderChild}
tnode={props.tnode}
/>
</TDefaultRenderer>
);
});
render(
<RenderHTML
source={{
html: '<div><p>One</p><p>Two</p></div>'
}}
debug={false}
renderers={{ div: DivRenderer }}
contentWidth={100}
/>
);
// The key encodes the full ancestor path: <p> at index 0 inside <div> at index 0
// inside synthetic <body> at index 0 inside TDocument (tagName "html") at index 0
expect(capturedKeys[0]).toBe(
'tnode_childTnode--p-0-div-0-body-0-html-0'
);
// Second <p> differs only in its own nodeIndex (1 instead of 0)
expect(capturedKeys[1]).toBe(
'tnode_childTnode--p-1-div-0-body-0-html-0'
);
});
it('should generate keys with the tnode_childTnode- prefix', () => {
const capturedKeys: string[] = [];
const renderChild = jest.fn(({ key }) => {
capturedKeys.push(key);
return null;
});
const DivRenderer: CustomTextualRenderer = jest.fn(function DivRenderer({
TDefaultRenderer,
...props
}) {
return (
<TDefaultRenderer {...props}>
<TNodeChildrenRenderer
renderChild={renderChild}
tnode={props.tnode}
/>
</TDefaultRenderer>
);
});
render(
<RenderHTML
source={{
html: '<div><p>One</p><p>Two</p></div>'
}}
debug={false}
renderers={{ div: DivRenderer }}
contentWidth={100}
/>
);
expect(capturedKeys.length).toBeGreaterThan(0);
for (const key of capturedKeys) {
expect(key).toMatch(/^tnode_childTnode-/);
}
});
});
});
12 changes: 11 additions & 1 deletion packages/render/src/renderChildren.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import TNodeRenderer from './TNodeRenderer';
import { TChildrenRendererProps } from './shared-types';
import collapseTopMarginForChild from './helpers/collapseTopMarginForChild';

function generateKey(childTnode: TNode): string {
let key = "";
let currNode = childTnode as TNode | null;
while (currNode){
key = `${key}-${currNode.tagName}-${String(currNode.nodeIndex)}`
currNode = currNode.parent;
}
return `childTnode-${key}`
}

const mapCollapsibleChildren = (
propsForChildren: TChildrenRendererProps['propsForChildren'],
renderChild: TChildrenRendererProps['renderChild'],
Expand All @@ -16,7 +26,7 @@ const mapCollapsibleChildren = (
? null
: collapseTopMarginForChild(n, tchildren);
const propsFromParent = { ...propsForChildren, collapsedMarginTop };
const key = childTnode.nodeIndex;
const key = `tnode_${generateKey(childTnode)}`;
const childElement = React.createElement(TNodeRenderer, {
propsFromParent,
tnode: childTnode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ exports[`TRenderEngine > buildTTree method should support disabling whitespace c
<TText tagName="span" nodeIndex={1} data="phrasing content" webStyles={{ whiteSpace: "normal"}} />
<TText anonymous nodeIndex={2} data="\\n" webStyles={{ whiteSpace: "normal"}} />
</TPhrasing>
<TBlock tagName="img" nodeIndex={3} src="https://domain.com/logo.jpg" webStyles={{ whiteSpace: "normal"}} />
<TPhrasing anonymous nodeIndex={0} webStyles={{ whiteSpace: "normal"}}>
<TText anonymous nodeIndex={4} data="\\n and this is " webStyles={{ whiteSpace: "normal"}} />
<TText tagName="strong" nodeIndex={5} data="too" webStyles={{ whiteSpace: "normal"}} />
<TText anonymous nodeIndex={6} data=".\\n" webStyles={{ whiteSpace: "normal"}} />
<TBlock tagName="img" nodeIndex={1} src="https://domain.com/logo.jpg" webStyles={{ whiteSpace: "normal"}} />
<TPhrasing anonymous nodeIndex={2} webStyles={{ whiteSpace: "normal"}}>
<TText anonymous nodeIndex={0} data="\\n and this is " webStyles={{ whiteSpace: "normal"}} />
<TText tagName="strong" nodeIndex={1} data="too" webStyles={{ whiteSpace: "normal"}} />
<TText anonymous nodeIndex={2} data=".\\n" webStyles={{ whiteSpace: "normal"}} />
</TPhrasing>
</TBlock>
</TBlock>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ exports[`hoist function should comply with RFC002 example (hoisting) 1`] = `
<TText tagName="span" nodeIndex={1} data="phrasing content" />
<TText anonymous nodeIndex={2} data="\\n" />
</TPhrasing>
<TBlock tagName="img" nodeIndex={3} src="https://domain.com/logo.jpg" />
<TPhrasing anonymous nodeIndex={0}>
<TText anonymous nodeIndex={4} data="\\n and this is " />
<TText tagName="strong" nodeIndex={5} data="too" />
<TText anonymous nodeIndex={6} data=".\\n" />
<TBlock tagName="img" nodeIndex={1} src="https://domain.com/logo.jpg" />
<TPhrasing anonymous nodeIndex={2}>
<TText anonymous nodeIndex={0} data="\\n and this is " />
<TText tagName="strong" nodeIndex={1} data="too" />
<TText anonymous nodeIndex={2} data=".\\n" />
</TPhrasing>
</TBlock>
`;

exports[`hoist function should hoist multiple blocks 1`] = `
<TBlock tagName="span" nodeIndex={0}>
<TBlock tagName="div" nodeIndex={0} />
<TPhrasing anonymous nodeIndex={0}>
<TText anonymous nodeIndex={1} data="Hello" />
<TPhrasing anonymous nodeIndex={1}>
<TText anonymous nodeIndex={0} data="Hello" />
</TPhrasing>
<TBlock tagName="img" nodeIndex={2} />
</TBlock>
Expand Down
8 changes: 4 additions & 4 deletions packages/transient-render-engine/src/flow/hoist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function groupText(tnode: TBlockImpl): TNodeImpl {
// some React Native styles working only for the uppermost Text element
// such as "textAlign" are preserved.
parentStyles: tnode.styles,
parent: null
parent: tnode
};
let wrapper = new TPhrasingCtor(wrapperInit);
let wrapperChildren: TNodeImpl[] = [];
Expand All @@ -26,18 +26,18 @@ function groupText(tnode: TBlockImpl): TNodeImpl {
} else {
if (wrapperChildren.length) {
newChildren.push(wrapper);
wrapper.bindChildren(wrapperChildren);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsamr I've checked the usage of nodeIndex, and it seems like the only thing that could break because of that is removing top and bottom margins in <li> components, but I tested it and it seems like it's working fine

wrapper.bindChildren(wrapperChildren, true);
wrapper = new TPhrasingCtor(wrapperInit);
wrapperChildren = [];
}
newChildren.push(child);
}
}
if (wrapperChildren.length) {
wrapper.bindChildren(wrapperChildren);
wrapper.bindChildren(wrapperChildren, true);
newChildren.push(wrapper);
}
tnode.bindChildren(newChildren);
tnode.bindChildren(newChildren, true);
return tnode;
}

Expand Down