Skip to content

Commit 9d648f1

Browse files
actions-userclaude
andcommitted
fix: prevent iframe content from being cloned twice
cloneIFrameElement (called via cloneSingleNode) already recursively clones the entire iframe contentDocument.body. The subsequent call to cloneChildren was then also iterating contentDocument.body.childNodes and appending them again, duplicating every child element. Fix: return early from cloneChildren when the native node is an iframe, since its subtree is fully handled by cloneIFrameElement. Closes #543 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a8def89 commit 9d648f1

File tree

3 files changed

+50
-5
lines changed

3 files changed

+50
-5
lines changed

src/clone-node.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@ async function cloneChildren<T extends HTMLElement>(
104104

105105
if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
106106
children = toArray<T>(nativeNode.assignedNodes())
107-
} else if (
108-
isInstanceOfElement(nativeNode, HTMLIFrameElement) &&
109-
nativeNode.contentDocument?.body
110-
) {
111-
children = toArray<T>(nativeNode.contentDocument.body.childNodes)
107+
} else if (isInstanceOfElement(nativeNode, HTMLIFrameElement)) {
108+
// cloneIFrameElement (called by cloneSingleNode) already recursively clones
109+
// the iframe's contentDocument.body and all its descendants.
110+
// Appending children here would duplicate the iframe content.
111+
return clonedNode
112112
} else {
113113
children = toArray<T>((nativeNode.shadowRoot ?? nativeNode).childNodes)
114114
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div id="iframe-wrapper" style="width:200px;height:80px;">
2+
<iframe
3+
id="test-iframe"
4+
style="width:100%;height:100%;border:none;"
5+
srcdoc="<html><body><p class='iframe-para' data-testid='iframe-para'>IFRAME_CONTENT</p></body></html>"
6+
></iframe>
7+
</div>

test/spec/special.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import '../spec/setup'
44
import { toPng } from '../../src'
5+
import { cloneNode } from '../../src/clone-node'
56
import { delay } from '../../src/util'
67
import { assertTextRendered, bootstrap, renderAndCheck } from '../spec/helper'
78

@@ -59,4 +60,41 @@ describe('special cases', () => {
5960
.then(done)
6061
.catch(done)
6162
})
63+
64+
it('should not duplicate iframe content when cloning', (done) => {
65+
// Regression test for: cloneChildren() re-appended iframe body childNodes
66+
// after cloneSingleNode() → cloneIFrameElement() had already recursively
67+
// cloned the full iframe body, causing every child to appear twice.
68+
bootstrap('iframe-content/node.html')
69+
.then((node) => {
70+
const iframe = node.querySelector('iframe') as HTMLIFrameElement
71+
// Poll until srcdoc iframe body is accessible (async load)
72+
return new Promise<HTMLDivElement>((resolve, reject) => {
73+
let attempts = 0
74+
const poll = () => {
75+
attempts++
76+
const ready =
77+
iframe.contentDocument?.body?.querySelector('.iframe-para')
78+
if (ready) {
79+
resolve(node)
80+
} else if (attempts > 30) {
81+
reject(new Error('iframe did not load in time'))
82+
} else {
83+
setTimeout(poll, 100)
84+
}
85+
}
86+
poll()
87+
})
88+
})
89+
.then((node) => cloneNode(node, {}, true))
90+
.then((clonedNode) => {
91+
expect(clonedNode).not.toBeNull()
92+
// With the bug: two .iframe-para elements appear (duplicate children).
93+
// With the fix: exactly one .iframe-para appears.
94+
const paras = clonedNode!.querySelectorAll('.iframe-para')
95+
expect(paras.length).toBe(1)
96+
})
97+
.then(done)
98+
.catch(done)
99+
})
62100
})

0 commit comments

Comments
 (0)