diff --git a/src/clone-node.ts b/src/clone-node.ts index 5dfcd117..b60cd5e5 100644 --- a/src/clone-node.ts +++ b/src/clone-node.ts @@ -88,11 +88,11 @@ async function cloneChildren( if (isSlotElement(nativeNode) && nativeNode.assignedNodes) { children = toArray(nativeNode.assignedNodes()) - } else if ( - isInstanceOfElement(nativeNode, HTMLIFrameElement) && - nativeNode.contentDocument?.body - ) { - children = toArray(nativeNode.contentDocument.body.childNodes) + } else if (isInstanceOfElement(nativeNode, HTMLIFrameElement)) { + // cloneIFrameElement (called by cloneSingleNode) already recursively clones + // the iframe's contentDocument.body and all its descendants. + // Appending children here would duplicate the iframe content. + return clonedNode } else { children = toArray((nativeNode.shadowRoot ?? nativeNode).childNodes) } diff --git a/test/resources/iframe-content/node.html b/test/resources/iframe-content/node.html new file mode 100644 index 00000000..46069d15 --- /dev/null +++ b/test/resources/iframe-content/node.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/test/spec/special.spec.ts b/test/spec/special.spec.ts index 6425195a..ebda2442 100644 --- a/test/spec/special.spec.ts +++ b/test/spec/special.spec.ts @@ -2,6 +2,7 @@ import '../spec/setup' import { toPng } from '../../src' +import { cloneNode } from '../../src/clone-node' import { delay } from '../../src/util' import { assertTextRendered, bootstrap, renderAndCheck } from '../spec/helper' @@ -59,4 +60,41 @@ describe('special cases', () => { .then(done) .catch(done) }) + + it('should not duplicate iframe content when cloning', (done) => { + // Regression test for: cloneChildren() re-appended iframe body childNodes + // after cloneSingleNode() → cloneIFrameElement() had already recursively + // cloned the full iframe body, causing every child to appear twice. + bootstrap('iframe-content/node.html') + .then((node) => { + const iframe = node.querySelector('iframe') as HTMLIFrameElement + // Poll until srcdoc iframe body is accessible (async load) + return new Promise((resolve, reject) => { + let attempts = 0 + const poll = () => { + attempts++ + const ready = + iframe.contentDocument?.body?.querySelector('.iframe-para') + if (ready) { + resolve(node) + } else if (attempts > 30) { + reject(new Error('iframe did not load in time')) + } else { + setTimeout(poll, 100) + } + } + poll() + }) + }) + .then((node) => cloneNode(node, {}, true)) + .then((clonedNode) => { + expect(clonedNode).not.toBeNull() + // With the bug: two .iframe-para elements appear (duplicate children). + // With the fix: exactly one .iframe-para appears. + const paras = clonedNode!.querySelectorAll('.iframe-para') + expect(paras.length).toBe(1) + }) + .then(done) + .catch(done) + }) })