diff --git a/packages/render/src/__tests__/component.render-html.test.tsx b/packages/render/src/__tests__/component.render-html.test.tsx index 36d6cf21..04d19b4e 100644 --- a/packages/render/src/__tests__/component.render-html.test.tsx +++ b/packages/render/src/__tests__/component.render-html.test.tsx @@ -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 ( + + + + ); + }); + render( +

One

Two

' + }} + 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 ( + + + + ); + }); + render( +

Nested One

Nested Two

' + }} + 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 ( + + + + ); + }); + render( +

One

Two

' + }} + debug={false} + renderers={{ div: DivRenderer }} + contentWidth={100} + /> + ); + // The key encodes the full ancestor path:

at index 0 inside

at index 0 + // inside synthetic 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

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 ( + + + + ); + }); + render( +

One

Two

' + }} + debug={false} + renderers={{ div: DivRenderer }} + contentWidth={100} + /> + ); + expect(capturedKeys.length).toBeGreaterThan(0); + for (const key of capturedKeys) { + expect(key).toMatch(/^tnode_childTnode-/); + } + }); + }); }); diff --git a/packages/render/src/renderChildren.tsx b/packages/render/src/renderChildren.tsx index be9ffd61..a143ed48 100644 --- a/packages/render/src/renderChildren.tsx +++ b/packages/render/src/renderChildren.tsx @@ -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'], @@ -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, diff --git a/packages/transient-render-engine/src/__tests__/__snapshots__/TRenderEngine.test.ts.snap b/packages/transient-render-engine/src/__tests__/__snapshots__/TRenderEngine.test.ts.snap index 20c6c04c..5c76cdd7 100644 --- a/packages/transient-render-engine/src/__tests__/__snapshots__/TRenderEngine.test.ts.snap +++ b/packages/transient-render-engine/src/__tests__/__snapshots__/TRenderEngine.test.ts.snap @@ -132,11 +132,11 @@ exports[`TRenderEngine > buildTTree method should support disabling whitespace c - - - - - + + + + + diff --git a/packages/transient-render-engine/src/flow/__tests__/__snapshots__/hoist.test.ts.snap b/packages/transient-render-engine/src/flow/__tests__/__snapshots__/hoist.test.ts.snap index 85dfe882..c872a77c 100644 --- a/packages/transient-render-engine/src/flow/__tests__/__snapshots__/hoist.test.ts.snap +++ b/packages/transient-render-engine/src/flow/__tests__/__snapshots__/hoist.test.ts.snap @@ -7,11 +7,11 @@ exports[`hoist function should comply with RFC002 example (hoisting) 1`] = ` - - - - - + + + + + `; @@ -19,8 +19,8 @@ exports[`hoist function should comply with RFC002 example (hoisting) 1`] = ` exports[`hoist function should hoist multiple blocks 1`] = ` - - + + diff --git a/packages/transient-render-engine/src/flow/hoist.ts b/packages/transient-render-engine/src/flow/hoist.ts index 59f78129..0ed65d34 100644 --- a/packages/transient-render-engine/src/flow/hoist.ts +++ b/packages/transient-render-engine/src/flow/hoist.ts @@ -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[] = []; @@ -26,7 +26,7 @@ function groupText(tnode: TBlockImpl): TNodeImpl { } else { if (wrapperChildren.length) { newChildren.push(wrapper); - wrapper.bindChildren(wrapperChildren); + wrapper.bindChildren(wrapperChildren, true); wrapper = new TPhrasingCtor(wrapperInit); wrapperChildren = []; } @@ -34,10 +34,10 @@ function groupText(tnode: TBlockImpl): TNodeImpl { } } if (wrapperChildren.length) { - wrapper.bindChildren(wrapperChildren); + wrapper.bindChildren(wrapperChildren, true); newChildren.push(wrapper); } - tnode.bindChildren(newChildren); + tnode.bindChildren(newChildren, true); return tnode; }