/
githubmirror
/
preact
Обзор
Документация
Войти
/
githubmirror
/
preact
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/browser/render.test.jsx
2 152 строки
52 KB
Jovi De Croock
Fix reusing template element content
05 авг 2026, 18:06
05 авг 2026, 18:06
b6d320a
Код
Авторство
О чём код?
import { setupRerender } from 'preact/test-utils'; import { createElement, render, Component, options, Fragment } from 'preact'; import { setupScratch, teardown, getMixedArray, mixedArrayHTML, serializeHtml, supportsDataList, sortAttributes, spyOnElementAttributes, createEvent } from '../_util/helpers'; import { clearLog, getLog, logCall } from '../_util/logCall'; import { expect, vi } from 'vitest'; function getAttributes(node) { let attrs = {}; for (let i = node.attributes.length; i--; ) { attrs[node.attributes[i].name] = node.attributes[i].value; } return attrs; } describe('render()', () => { let scratch, rerender; let resetAppendChild; let resetInsertBefore; let resetRemoveText; let resetRemove; let resetMoveBefore; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); beforeAll(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); // @ts-expect-error resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetRemoveText = logCall(Text.prototype, 'remove'); resetRemove = logCall(Element.prototype, 'remove'); }); afterAll(() => { resetAppendChild(); resetMoveBefore(); resetInsertBefore(); resetRemoveText(); resetRemove(); }); it('should rerender when value from "" to 0', () => { render('', scratch); expect(scratch.innerHTML).to.equal(''); render(0, scratch); expect(scratch.innerHTML).to.equal('0'); }); it('should render an empty text node given an empty string', () => { render('', scratch); let c = scratch.childNodes; expect(c).to.have.length(1); expect(c[0].data).to.equal(''); expect(c[0].nodeName).to.equal('#text'); }); it('should allow node type change with content', () => { render(<span>Bad</span>, scratch); render(<div>Good</div>, scratch); expect(scratch.innerHTML).to.eql(`<div>Good</div>`); }); it('should render % width and height on img correctly', () => { render(<img width="100%" height="100%" />, scratch); expect(scratch.innerHTML).to.eql(`<img width="100%" height="100%">`); }); it('should render px width and height on img correctly', () => { render(<img width="100px" height="100px" />, scratch); expect(scratch.innerHTML).to.eql(`<img width="100px" height="100px">`); }); it('should support the <template> tag', () => { function App() { return ( <template> <h1>it works</h1> </template> ); } render(<App />, scratch); const clone = scratch.firstChild.content.cloneNode(true); expect(clone.firstChild.outerHTML).to.eql('<h1>it works</h1>'); }); it('should reuse existing <template> content', () => { function App() { return ( <template> <h1>it works</h1> </template> ); } scratch.innerHTML = `<template><h1>it works</h1></template>`; const template = scratch.firstChild; const h1 = template.content.firstChild; render(<App />, scratch); expect(scratch.firstChild).to.equal(template); expect(template.content.childNodes).to.have.length(1); expect(template.content.firstChild).to.equal(h1); }); it('should update the children of a <template>', () => { function App({ items }) { return ( <div> <template> {items.map(i => ( <h1 key={i}>{i}</h1> ))} </template> <p>after</p> </div> ); } render(<App items={[1, 2]} />, scratch); let template = scratch.firstChild.firstChild; expect(template.content.textContent).to.equal('12'); render(<App items={[1, 2, 3]} />, scratch); expect(template.content.textContent).to.equal('123'); render(<App items={[3]} />, scratch); expect(template.content.textContent).to.equal('3'); expect(scratch.innerHTML).to.equal( '<div><template><h1>3</h1></template><p>after</p></div>' ); }); it('should render function components with an undefined prototype', () => { const Foo = () => <div>foo</div>; Foo.prototype = undefined; render(<Foo />, scratch); expect(scratch.innerHTML).to.equal('<div>foo</div>'); }); it('should not render when detecting JSON-injection', () => { const vnode = JSON.parse( '{"type":"span","props":{ "children": "Malicious"}, "__v": 1, "constructor": null}' ); render(vnode, scratch); expect(scratch.firstChild).to.be.null; }); it('should not render when detecting JSON-injection deeply', () => { const payload = JSON.parse( '{"type":"span","props":{ "children": "Malicious"}, "__v": 1, "__proto__": null}' ); const App = () => <Fragment>{payload}</Fragment>; render(<App />, scratch); console.log(scratch.innerHTML); expect(scratch.firstChild).to.be.null; expect(scratch.innerHTML).to.equal(''); }); it('should create empty nodes (<* />)', () => { render(<div />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.childNodes[0].nodeName).to.equal('DIV'); scratch.parentNode.removeChild(scratch); scratch = document.createElement('div'); (document.body || document.documentElement).appendChild(scratch); render(<span />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.childNodes[0].nodeName).to.equal('SPAN'); }); it('should support custom tag names', () => { render(<foo />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.firstChild).to.have.property('nodeName', 'FOO'); scratch.parentNode.removeChild(scratch); scratch = document.createElement('div'); (document.body || document.documentElement).appendChild(scratch); render(<x-bar />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.firstChild).to.have.property('nodeName', 'X-BAR'); }); it('should support the translate attribute w/ false as a boolean', () => { render(<b translate={false}>Bold</b>, scratch); expect(scratch.innerHTML).to.equal('<b translate="no">Bold</b>'); }); it('should support the translate attribute w/ true as a boolean', () => { render(<b translate>Bold</b>, scratch); expect(scratch.innerHTML).to.equal('<b translate="yes">Bold</b>'); }); it('should support the form attribute', () => { render( <div> <form id="myform" /> <button form="myform">test</button> <input form="myform" /> </div>, scratch ); const div = scratch.childNodes[0]; const form = div.childNodes[0]; const button = div.childNodes[1]; const input = div.childNodes[2]; expect(button).to.have.property('form', form); expect(input).to.have.property('form', form); }); it('should allow VNode reuse', () => { let reused = <div class="reuse">Hello World!</div>; render( <div> {reused} <hr /> {reused} </div>, scratch ); expect(serializeHtml(scratch)).to.eql( `<div><div class="reuse">Hello World!</div><hr><div class="reuse">Hello World!</div></div>` ); render( <div> <hr /> {reused} </div>, scratch ); expect(serializeHtml(scratch)).to.eql( `<div><hr><div class="reuse">Hello World!</div></div>` ); }); it('should merge new elements when called multiple times', () => { render(<div />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.firstChild).to.have.property('nodeName', 'DIV'); expect(scratch.innerHTML).to.equal('<div></div>'); render(<span />, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.firstChild).to.have.property('nodeName', 'SPAN'); expect(scratch.innerHTML).to.equal('<span></span>'); render(<span class="hello">Hello!</span>, scratch); expect(scratch.childNodes).to.have.length(1); expect(scratch.firstChild).to.have.property('nodeName', 'SPAN'); expect(scratch.innerHTML).to.equal('<span class="hello">Hello!</span>'); }); it('should not keep the root input vnode in props.children', () => { let update; class App extends Component { constructor(props) { super(props); this.state = { value: 'a' }; update = this.setState.bind(this); } render() { return <button>{this.state.value}</button>; } } render(<App />, scratch); let firstAppVNode = scratch._children._children[0]; update({ value: 'b' }); rerender(); expect(scratch._children._children[0]).to.not.equal(firstAppVNode); expect(scratch._children.props.children).to.equal(null); }); it('should nest empty nodes', () => { render( <div> <span /> <foo /> <x-bar /> </div>, scratch ); expect(scratch.childNodes).to.have.length(1); expect(scratch.childNodes[0].nodeName).to.equal('DIV'); let c = scratch.childNodes[0].childNodes; expect(c).to.have.length(3); expect(c[0].nodeName).to.equal('SPAN'); expect(c[1].nodeName).to.equal('FOO'); expect(c[2].nodeName).to.equal('X-BAR'); }); it('should not render falsy values', () => { render( <div> {null},{undefined},{false},{0},{NaN} </div>, scratch ); expect(scratch.firstChild).to.have.property('innerHTML', ',,,0,NaN'); }); it('should not render null', () => { render(null, scratch); expect(scratch.innerHTML).to.equal(''); expect(scratch.childNodes).to.have.length(0); }); it('should not render undefined', () => { render(undefined, scratch); expect(scratch.innerHTML).to.equal(''); expect(scratch.childNodes).to.have.length(0); }); it('should not render boolean true', () => { render(true, scratch); expect(scratch.innerHTML).to.equal(''); expect(scratch.childNodes).to.have.length(0); }); it('should not render boolean false', () => { render(false, scratch); expect(scratch.innerHTML).to.equal(''); expect(scratch.childNodes).to.have.length(0); }); it('should not render children when using function children', () => { render(<div>{() => {}}</div>, scratch); expect(scratch.innerHTML).to.equal('<div></div>'); }); it('should not render children when rerendering a function child', () => { const icon = () => {}; render(<div>{icon}</div>, scratch); expect(scratch.innerHTML).to.equal('<div></div>'); render(<div>{icon}</div>, scratch); expect(scratch.innerHTML).to.equal('<div></div>'); }); it('should render NaN as text content', () => { render(NaN, scratch); expect(scratch.innerHTML).to.equal('NaN'); }); it('should render numbers (0) as text content', () => { render(0, scratch); expect(scratch.innerHTML).to.equal('0'); }); it('should render numbers (42) as text content', () => { render(42, scratch); expect(scratch.innerHTML).to.equal('42'); }); it('should render bigint as text content', () => { // Skip in browsers not supporting big integers if (typeof BigInt === 'undefined') { return; } // eslint-disable-next-line no-undef, new-cap render(BigInt(4), scratch); expect(scratch.innerHTML).to.equal('4'); }); it('should render strings as text content', () => { render('Testing, huh! How is it going?', scratch); expect(scratch.innerHTML).to.equal('Testing, huh! How is it going?'); }); it('should render arrays of mixed elements', () => { render(getMixedArray(), scratch); expect(scratch.innerHTML).to.equal(mixedArrayHTML); }); it('should clear falsy attributes', () => { render( <div anull="anull" aundefined="aundefined" afalse="afalse" anan="aNaN" a0="a0" />, scratch ); render( <div anull={null} aundefined={undefined} afalse={false} anan={NaN} a0={0} />, scratch ); expect( getAttributes(scratch.firstChild), 'from previous truthy values' ).to.eql({ a0: '0', anan: 'NaN' }); }); it('should not render falsy attributes on hydrate', () => { render( <div anull={null} aundefined={undefined} afalse={false} anan={NaN} a0={0} />, scratch ); expect(getAttributes(scratch.firstChild), 'initial render').to.eql({ a0: '0', anan: 'NaN' }); }); it('should clear falsy input values', () => { // Note: this test just demonstrates the default browser behavior render( <div> <input value={0} /> <input value={false} /> <input value={null} /> <input value={undefined} /> </div>, scratch ); let root = scratch.firstChild; expect(root.children[0]).to.have.property('value', '0'); expect(root.children[1]).to.have.property('value', 'false'); expect(root.children[2]).to.have.property('value', ''); expect(root.children[3]).to.have.property('value', ''); }); it('should set value inside the specified range', () => { render( <input type="range" value={0.5} min="0" max="1" step="0.05" />, scratch ); expect(scratch.firstChild.value).to.equal('0.5'); }); it('should not clear falsy DOM properties', () => { function test(val) { render( <div> <input value={val} /> <table border={val} /> </div>, scratch ); } test('2'); test(false); expect(scratch.innerHTML).to.equal( '<div><input><table border="false"></table></div>', 'for false' ); test('3'); test(null); expect(scratch.innerHTML).to.equal( '<div><input><table border=""></table></div>', 'for null' ); test('4'); test(undefined); expect(scratch.innerHTML).to.equal( '<div><input><table border=""></table></div>', 'for undefined' ); }); // Test for #3969 it('should clear rowspan and colspan', () => { /** @type {(v) => void} */ let update; class App extends Component { constructor(props) { super(props); this.state = { active: true }; update = this.setState.bind(this); } render() { return ( <div> {this.state.active ? ( <table> <tr> <td rowSpan={2} colSpan={2}> Foo </td> </tr> </table> ) : ( <table> <tr> <td>Foo</td> </tr> </table> )} </div> ); } } render(<App />, scratch); update({ active: false }); rerender(); expect(scratch.querySelector('td[rowspan]')).to.equal(null); expect(scratch.querySelector('td[colspan]')).to.equal(null); }); // Test for preactjs/preact#651 it('should set enumerable boolean attribute', () => { render(<input spellcheck={false} />, scratch); expect(scratch.firstChild.spellcheck).to.equal(false); }); it('should support popover auto', () => { render(<div popover="auto" />, scratch); expect(scratch.innerHTML).to.equal('<div popover="auto"></div>'); }); it('should support popover true boolean', () => { render(<div popover />, scratch); expect(scratch.innerHTML).to.equal('<div popover=""></div>'); }); it('should support popover false boolean', () => { render(<div popover={false} />, scratch); expect(scratch.innerHTML).to.equal('<div></div>'); }); // Test for preactjs/preact#4340 it('should respect defaultValue in render', () => { scratch.innerHTML = '<input value="foo">'; render(<input defaultValue="foo" />, scratch); expect(scratch.firstChild.value).to.equal('foo'); }); it('should support subsequent renders w/ defaultValue', () => { scratch.innerHTML = '<input value="foo">'; render(<input defaultValue="foo" value="bar" />, scratch); expect(scratch.firstChild.value).to.equal('bar'); render(<input defaultValue="foo" value="baz" />, scratch); expect(scratch.firstChild.value).to.equal('baz'); }); it('should respect defaultChecked in render', () => { scratch.innerHTML = '<input checked="true">'; render(<input defaultChecked />, scratch); expect(scratch.firstChild.checked).to.equal(true); }); it('should support subsequent renders w/ defaultChecked', () => { scratch.innerHTML = '<input checked="true">'; render(<input defaultChecked checked />, scratch); expect(scratch.firstChild.checked).to.equal(true); render(<input defaultChecked checked={false} />, scratch); expect(scratch.firstChild.checked).to.equal(false); }); it('should not try to set element.children', () => { render( <div> <span /> </div>, scratch ); const spy = vi.fn(); Object.defineProperty(scratch.firstChild, 'children', { set: spy }); render(<div />, scratch); expect(spy).not.toHaveBeenCalled(); }); it('should render download attribute', () => { render(<a download="" />, scratch); expect(scratch.firstChild.getAttribute('download')).to.equal(''); render(<a download={null} />, scratch); expect(scratch.firstChild.getAttribute('download')).to.equal(null); }); it('should not set tagName', () => { expect(() => render(<input tagName="div" />, scratch)).not.to.throw(); }); it('should apply string attributes', () => { render(<div foo="bar" data-foo="databar" />, scratch); expect(serializeHtml(scratch)).to.equal( '<div data-foo="databar" foo="bar"></div>' ); }); it('should not serialize function props as attributes', () => { render(<div click={function a() {}} ONCLICK={function b() {}} />, scratch); let div = scratch.childNodes[0]; expect(div.attributes.length).to.equal(0); }); it('should serialize object props as attributes', () => { render( <div foo={{ a: 'b' }} bar={{ toString() { return 'abc'; } }} />, scratch ); let div = scratch.childNodes[0]; expect(div.attributes.length).to.equal(2); // Normalize attribute order because it's different in various browsers let normalized = {}; for (let i = 0; i < div.attributes.length; i++) { let attr = div.attributes[i]; normalized[attr.name] = attr.value; } expect(normalized).to.deep.equal({ bar: 'abc', foo: '[object Object]' }); }); it('should apply class as String', () => { render(<div class="foo" />, scratch); expect(scratch.childNodes[0]).to.have.property('className', 'foo'); }); it('should alias className to class', () => { render(<div className="bar" />, scratch); expect(scratch.childNodes[0]).to.have.property('className', 'bar'); }); it('should support false string aria-* attributes', () => { render(<div aria-checked="false" />, scratch); expect(scratch.firstChild.getAttribute('aria-checked')).to.equal('false'); }); it('should support false aria-* attributes', () => { render(<div aria-checked={false} />, scratch); expect(scratch.firstChild.getAttribute('aria-checked')).to.equal('false'); }); it('should support false data-* attributes', () => { render(<div data-checked={false} />, scratch); expect(scratch.firstChild.getAttribute('data-checked')).to.equal('false'); }); it('should set checked attribute on custom elements without checked property', () => { render(<o-checkbox checked />, scratch); expect(scratch.innerHTML).to.equal( '<o-checkbox checked="true"></o-checkbox>' ); }); it('should set value attribute on custom elements without value property', () => { render(<o-input value="test" />, scratch); expect(scratch.innerHTML).to.equal('<o-input value="test"></o-input>'); }); it('should mask value on password input elements', () => { render(<input value="xyz" type="password" />, scratch); expect(scratch.innerHTML).to.equal('<input type="password">'); }); it('should unset href if null || undefined', () => { render( <pre> <a href="#">href="#"</a> <a href={undefined}>href="undefined"</a> <a href={null}>href="null"</a> <a href={''}>href="''"</a> </pre>, scratch ); const links = scratch.querySelectorAll('a'); expect(links[0].hasAttribute('href')).to.equal(true); expect(links[1].hasAttribute('href')).to.equal(false); expect(links[2].hasAttribute('href')).to.equal(false); expect(links[3].hasAttribute('href')).to.equal(true); }); describe('dangerouslySetInnerHTML', () => { it('should support dangerouslySetInnerHTML', () => { let html = '<b>foo & bar</b>'; // eslint-disable-next-line react/no-danger render(<div dangerouslySetInnerHTML={{ __html: html }} />, scratch); expect(scratch.firstChild, 'set').to.have.property('innerHTML', html); expect(scratch.innerHTML).to.equal('<div>' + html + '</div>'); render( <div> a<strong>b</strong> </div>, scratch ); expect(scratch, 'unset').to.have.property( 'innerHTML', `<div>a<strong>b</strong></div>` ); // eslint-disable-next-line react/no-danger render(<div dangerouslySetInnerHTML={{ __html: html }} />, scratch); expect(scratch.innerHTML, 're-set').to.equal('<div>' + html + '</div>'); }); it('should apply proper mutation for VNodes with dangerouslySetInnerHTML attr', () => { /** @type {Component} */ let thing; class Thing extends Component { constructor(props, context) { super(props, context); this.state = { html: this.props.html }; thing = this; } render(props, { html }) { // eslint-disable-next-line react/no-danger return html ? ( <div dangerouslySetInnerHTML={{ __html: html }} /> ) : ( <div /> ); } } render(<Thing html="<b><i>test</i></b>" />, scratch); expect(scratch.innerHTML).to.equal('<div><b><i>test</i></b></div>'); thing.setState({ html: false }); rerender(); expect(scratch.innerHTML).to.equal('<div></div>'); thing.setState({ html: '<foo><bar>test</bar></foo>' }); rerender(); expect(scratch.innerHTML).to.equal( '<div><foo><bar>test</bar></foo></div>' ); }); it('should not hydrate with dangerouslySetInnerHTML', () => { let html = '<b>foo & bar</b>'; scratch.innerHTML = `<div>${html}</div>`; // eslint-disable-next-line react/no-danger render(<div dangerouslySetInnerHTML={{ __html: html }} />, scratch); expect(scratch.firstChild).to.have.property('innerHTML', html); expect(scratch.innerHTML).to.equal(`<div>${html}</div>`); }); it('should avoid reapplying innerHTML when __html property of dangerouslySetInnerHTML attr remains unchanged', () => { /** @type {Component} */ let thing; class Thing extends Component { constructor(props) { super(props); thing = this; } render() { // eslint-disable-next-line react/no-danger return ( <div dangerouslySetInnerHTML={{ __html: '<span>same</span>' }} /> ); } } render(<Thing />, scratch); let firstInnerHTMLChild = scratch.firstChild.firstChild; // Re-render thing.forceUpdate(); expect(firstInnerHTMLChild).to.equalNode(scratch.firstChild.firstChild); }); it('should unmount dangerouslySetInnerHTML', () => { /** @type {(v) => void} */ let set; const TextDiv = () => ( <div dangerouslySetInnerHTML={{ __html: '' }}>some text</div> ); class App extends Component { constructor(props) { super(props); set = this.setState.bind(this); this.state = { show: true }; } render() { return this.state.show && <TextDiv />; } } render(<App />, scratch); expect(scratch.innerHTML).to.equal('<div></div>'); set({ show: false }); rerender(); expect(scratch.innerHTML).to.equal(''); }); }); it('should reconcile mutated DOM attributes', () => { let check = p => render(<input type="checkbox" checked={p} />, scratch), value = () => scratch.lastChild.checked, setValue = p => (scratch.lastChild.checked = p); check(true); expect(value()).to.equal(true); check(false); expect(value()).to.equal(false); check(true); expect(value()).to.equal(true); setValue(true); check(false); expect(value()).to.equal(false); setValue(false); check(true); expect(value()).to.equal(true); }); it('should reorder child pairs', () => { render( <div> <a>a</a> <b>b</b> </div>, scratch ); let a = scratch.firstChild.firstChild; let b = scratch.firstChild.lastChild; expect(a).to.have.property('nodeName', 'A'); expect(b).to.have.property('nodeName', 'B'); render( <div> <b>b</b> <a>a</a> </div>, scratch ); expect(scratch.firstChild.firstChild).to.equalNode(b); expect(scratch.firstChild.lastChild).to.equalNode(a); }); // Discussion: https://github.com/preactjs/preact/issues/287 // <datalist> is not supported in Safari, even though the element // constructor is present if (supportsDataList()) { it('should allow <input list /> to pass through as an attribute', () => { render( <div> <input type="range" min="0" max="100" list="steplist" /> <datalist id="steplist"> <option>0</option> <option>50</option> <option>100</option> </datalist> </div>, scratch ); let html = scratch.firstElementChild.firstElementChild.outerHTML; expect(sortAttributes(html)).to.equal( sortAttributes('<input type="range" min="0" max="100" list="steplist">') ); }); } // Issue #2284 it('should not throw when setting size to an invalid value', () => { // These values are usually used to reset the `size` attribute to its // initial state. expect(() => render(<input size={undefined} />, scratch)).to.not.throw(); expect(() => render(<input size={null} />, scratch)).to.not.throw(); expect(() => render(<input size={0} />, scratch)).to.not.throw(); }); it('should not execute append operation when child is at last', () => { // See preactjs/preact#717 for discussion about the issue this addresses let todoText = 'new todo that I should complete'; let input; /** @type {(v) => void} */ let setText; /** @type {(v) => void} */ let addTodo; const ENTER = 13; class TodoList extends Component { constructor(props) { super(props); this.state = { todos: [], text: '' }; setText = this.setText = this.setText.bind(this); addTodo = this.addTodo = this.addTodo.bind(this); } setText(e) { this.setState({ text: e.target.value }); } addTodo(e) { if (e.keyCode === ENTER) { let { todos, text } = this.state; todos = todos.concat({ text }); this.setState({ todos, text: '' }); } } render() { const { todos, text } = this.state; return ( <div onKeyDown={this.addTodo}> {todos.map(todo => [ <span>{todo.text}</span>, <span> {' '} [ <a href="javascript:;">Delete</a> ] </span>, <br /> ])} <input value={text} onInput={this.setText} ref={i => (input = i)} /> </div> ); } } render(<TodoList />, scratch); // Simulate user typing input.focus(); input.value = todoText; setText({ target: input }); // Commit the user typing setState call rerender(); // Simulate user pressing enter addTodo({ keyCode: ENTER }); // Before Preact rerenders, focus should be on the input expect(document.activeElement).to.equalNode(input); rerender(); // After Preact rerenders, focus should remain on the input expect(document.activeElement).to.equalNode(input); expect(scratch.innerHTML).to.contain(`<span>${todoText}</span>`); }); it('should keep value of uncontrolled inputs', () => { render(<input value={undefined} />, scratch); scratch.firstChild.value = 'foo'; render(<input value={undefined} />, scratch); expect(scratch.firstChild.value).to.equal('foo'); }); it('should keep value of uncontrolled checkboxes', () => { render(<input type="checkbox" checked={undefined} />, scratch); scratch.firstChild.checked = true; render(<input type="checkbox" checked={undefined} />, scratch); expect(scratch.firstChild.checked).to.equal(true); }); // #2756 it('should set progress value to 0', () => { render(<progress value={0} max="100" />, scratch); expect(scratch.firstChild.value).to.equal(0); expect(scratch.firstChild.getAttribute('value')).to.equal('0'); }); // #4487 it('should not set value for undefined/null on a progress element', () => { render(<progress value={undefined} />, scratch); expect(scratch.firstChild.getAttribute('value')).to.equal(null); render(<progress value={null} />, scratch); expect(scratch.firstChild.getAttribute('value')).to.equal(null); render(<progress value={0} />, scratch); expect(scratch.firstChild.getAttribute('value')).to.equal('0'); render(<progress value={50} />, scratch); expect(scratch.firstChild.getAttribute('value')).to.equal('50'); render(<progress value={null} />, scratch); expect(scratch.firstChild.getAttribute('value')).to.equal(null); }); it('should always diff `checked` and `value` properties against the DOM', () => { // See https://github.com/preactjs/preact/issues/1324 let inputs; let text; let checkbox; class Inputs extends Component { constructor(props) { super(props); inputs = this; } render() { return ( <div> <input value={'Hello'} ref={el => (text = el)} /> <input type="checkbox" checked ref={el => (checkbox = el)} /> </div> ); } } render(<Inputs />, scratch); expect(text.value).to.equal('Hello'); expect(checkbox.checked).to.equal(true); text.value = 'World'; checkbox.checked = false; inputs.forceUpdate(); rerender(); expect(text.value).to.equal('Hello'); expect(checkbox.checked).to.equal(true); }); it('should always diff `contenteditable` `innerHTML` against the DOM', () => { // This tests that we do not cause any cursor jumps in contenteditable fields // See https://github.com/preactjs/preact/issues/2691 class Editable extends Component { constructor(props) { super(props); this.state = { value: 'Hello' }; } render(props, state) { return ( <div contentEditable dangerouslySetInnerHTML={{ __html: state.value }} onInput={e => this.setState({ value: e.currentTarget.innerHTML })} /> ); } } render(<Editable />, scratch); let editable = scratch.querySelector('[contenteditable]'); // modify the innerHTML and set the caret to character 2 to simulate a user typing editable.innerHTML = 'World'; const range = document.createRange(); range.selectNodeContents(editable); range.setStart(editable.childNodes[0], 2); range.collapse(true); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); // ensure we didn't mess up setting the cursor to position 2 expect(window.getSelection().getRangeAt(0).startOffset).to.equal(2); // dispatch the input event to tell preact to re-render editable.dispatchEvent(createEvent('input')); rerender(); // ensure innerHTML is still correct (was not an issue before) and // more importantly the caret is still at character 2 editable = scratch.querySelector('[contenteditable]'); expect(editable.innerHTML).to.equal('World'); expect(window.getSelection().getRangeAt(0).startOffset).to.equal(2); }); it('should not re-render when a component returns undefined', () => { let Dialog = () => undefined; /** @type {() => void} */ let updateState; class App extends Component { constructor(props) { super(props); this.state = { name: '' }; updateState = () => this.setState({ name: ', friend' }); } render(props, { name }) { return ( <div> <Dialog /> <h1 class="fade-down">Hi{name}</h1> </div> ); } } render(<App />, scratch); clearLog(); updateState(); rerender(); // We don't log text updates expect(getLog()).to.deep.equal([]); }); it('should not lead to stale DOM nodes', () => { let i = 0; /** @type {() => void} */ let updateApp; class App extends Component { render() { updateApp = () => this.forceUpdate(); return <Parent />; } } /** @type {() => void} */ let updateParent; function Parent() { updateParent = () => this.forceUpdate(); i++; return <Child i={i} />; } function Child({ i }) { return i < 3 ? null : <div>foo</div>; } render(<App />, scratch); updateApp(); rerender(); updateParent(); rerender(); updateApp(); rerender(); // Without a fix it would render: `<div>foo</div><div></div>` expect(scratch.innerHTML).to.equal('<div>foo</div>'); }); // see preact/#1327 it('should not reuse unkeyed components', () => { let ref; class X extends Component { constructor() { super(); ref = this; this.id = null; this.state = { i: 0 }; } update() { this.setState(prev => ({ i: prev.i + 1 })); } componentWillUnmount() { clearTimeout(this.id); } render() { return <div>{this.state.i}</div>; } } /** @type {() => void} */ let updateApp; class App extends Component { constructor() { super(); this.state = { i: 0 }; updateApp = () => this.setState(prev => ({ i: prev.i ^ 1 })); } render() { return ( <div> {this.state.i === 0 && <X />} <X /> </div> ); } } render(<App />, scratch); expect(scratch.textContent).to.equal('00'); ref.update(); updateApp(); rerender(); expect(scratch.textContent).to.equal('1'); updateApp(); rerender(); expect(scratch.textContent).to.equal('01'); }); it('should not remove iframe', () => { /** @type {(v) => void} */ let setState; const Iframe = () => { // oxlint-disable-next-line iframe-missing-sandbox return <iframe src="https://codesandbox.io/s/runtime-silence-no4zx" />; }; const Test2 = () => <div>Test2</div>; const Test3 = () => <div>Test3</div>; class App extends Component { constructor(props) { super(props); this.state = { value: true }; setState = this.setState.bind(this); } render(props, state) { return ( <div> {state.value ? <Test3 /> : null} {state.value ? <Test2 /> : null} <Iframe key="iframe" /> </div> ); } } render(<App />, scratch); expect(scratch.innerHTML).to.equal( '<div><div>Test3</div><div>Test2</div><iframe src="https://codesandbox.io/s/runtime-silence-no4zx"></iframe></div>' ); clearLog(); setState({ value: false }); rerender(); expect(scratch.innerHTML).to.equal( '<div><iframe src="https://codesandbox.io/s/runtime-silence-no4zx"></iframe></div>' ); expect(getLog()).to.deep.equal([ '<div>Test3.remove()', '<div>Test2.remove()' ]); }); it('should not cause infinite loop with referentially equal props', () => { let i = 0; let prevDiff = options._diff; options._diff = () => { if (++i > 10) { options._diff = prevDiff; throw new Error('Infinite loop'); } }; function App({ children, ...rest }) { return ( <div {...rest}> <div {...rest}>{children}</div> </div> ); } render(<App>10</App>, scratch); expect(scratch.textContent).to.equal('10'); options._diff = prevDiff; }); it('should not call options.debounceRendering unnecessarily', () => { /** @type {A} */ let comp; class A extends Component { constructor(props) { super(props); this.state = { updates: 0 }; comp = this; } render() { return <div>{this.state.updates}</div>; } } render(<A />, scratch); expect(scratch.innerHTML).to.equal('<div>0</div>'); const debounceSpy = vi.spyOn(options, 'debounceRendering'); try { comp.setState({ updates: 1 }, () => { comp.setState({ updates: 2 }); }); rerender(); expect(scratch.innerHTML).to.equal('<div>2</div>'); expect(debounceSpy).toHaveBeenCalledTimes(1); } finally { debounceSpy.mockRestore(); } }); it('should remove attributes on pre-existing DOM', () => { const div = document.createElement('div'); div.setAttribute('class', 'red'); const span = document.createElement('span'); const text = document.createTextNode('Hi'); span.appendChild(text); div.appendChild(span); scratch.appendChild(div); const App = () => ( <div> <span>Bye</span> </div> ); render(<App />, scratch); expect(serializeHtml(scratch)).to.equal('<div><span>Bye</span></div>'); }); it('should remove class attributes', () => { const App = props => ( <div className={props.class}> <span>Bye</span> </div> ); render(<App class="hi" />, scratch); expect(scratch.innerHTML).to.equal( '<div class="hi"><span>Bye</span></div>' ); render(<App />, scratch); expect(serializeHtml(scratch)).to.equal('<div><span>Bye</span></div>'); }); it('should not read DOM attributes on render without existing DOM', () => { const attributesSpy = spyOnElementAttributes(); render( <div id="wrapper"> <div id="page1">Page 1</div> </div>, scratch ); expect(scratch.innerHTML).to.equal( '<div id="wrapper"><div id="page1">Page 1</div></div>' ); expect(attributesSpy).not.toHaveBeenCalled(); render( <div id="wrapper"> <div id="page2">Page 2</div> </div>, scratch ); expect(scratch.innerHTML).to.equal( '<div id="wrapper"><div id="page2">Page 2</div></div>' ); expect(attributesSpy).not.toHaveBeenCalled(); }); // #2926 it('should not throw when changing contentEditable to undefined or null', () => { render(<p contentEditable>foo</p>, scratch); expect(() => render(<p contentEditable={undefined}>foo</p>, scratch) ).to.not.throw(); expect(scratch.firstChild.contentEditable).to.equal('inherit'); expect(() => render(<p contentEditable={null}>foo</p>, scratch) ).to.not.throw(); expect(scratch.firstChild.contentEditable).to.equal('inherit'); }); // #2926 Part 2 it('should allow setting contentEditable to false', () => { render( <div contentEditable> <span>editable</span> <p contentEditable={false}>not editable</p> </div>, scratch ); expect(scratch.firstChild.contentEditable).to.equal('true'); expect(scratch.querySelector('p').contentEditable).to.equal('false'); }); // #3060 it('should reset tabindex on undefined/null', () => { const defaultValue = -1; render(<div tabIndex={0} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(0); render(<div tabIndex={undefined} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(defaultValue); render(<div tabIndex={null} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(defaultValue); render(<div tabindex={0} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(0); render(<div tabindex={undefined} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(defaultValue); render(<div tabindex={null} />, scratch); expect(scratch.firstChild.tabIndex).to.equal(defaultValue); }); // #4137 it('should unset role if null || undefined', () => { render( <section> <div role="status">role="status"</div> <div role={undefined}>role="undefined"</div> <div role={null}>role="null"</div> </section>, scratch ); const divs = scratch.querySelectorAll('div'); expect(divs[0].hasAttribute('role')).to.equal(true); expect(divs[1].hasAttribute('role')).to.equal(false); expect(divs[2].hasAttribute('role')).to.equal(false); }); it('should not crash or repeatedly add the same child when replacing a matched vnode with null', () => { const B = () => <div>B</div>; /** @type {() => void} */ let update; class App extends Component { constructor(props) { super(props); this.state = { show: true }; update = () => { this.setState(state => ({ show: !state.show })); }; } render() { if (this.state.show) { return ( <div> <B /> <div /> </div> ); } return ( <div> <div /> {null} <B /> </div> ); } } render(<App />, scratch); expect(scratch.innerHTML).to.equal('<div><div>B</div><div></div></div>'); update(); rerender(); expect(scratch.innerHTML).to.equal('<div><div></div><div>B</div></div>'); update(); rerender(); expect(scratch.innerHTML).to.equal('<div><div>B</div><div></div></div>'); update(); rerender(); expect(scratch.innerHTML).to.equal('<div><div></div><div>B</div></div>'); }); it('should reconcile children in right order', () => { let data = ['A', 'B', 'C', 'D', 'E']; render( <ul> {data.map(d => ( <li key={d}>{d}</li> ))} </ul>, scratch ); expect(scratch.textContent).to.equal('ABCDE'); data = ['B', 'E', 'C', 'D']; render( <ul> {data.map(d => ( <li key={d}>{d}</li> ))} </ul>, scratch ); expect(scratch.textContent).to.equal('BECD'); }); it('should reconcile children in right order #2', () => { let data = ['A', 'B', 'C', 'D', 'E']; render( <ul> {data.map(d => ( <li key={d}>{d}</li> ))} </ul>, scratch ); expect(scratch.textContent).to.equal('ABCDE'); data = ['B', 'E', 'D', 'C']; render( <ul> {data.map(d => ( <li key={d}>{d}</li> ))} </ul>, scratch ); expect(scratch.textContent).to.equal('BEDC'); }); it('should reconcile children in right order #3', () => { render( <div> <p>_A1</p> <p>_A2</p> <h2>_A3</h2> <p>_A4</p> <h2>_A5</h2> <p>_A6</p> <h2>_A7</h2> <p>_A8</p> </div>, scratch ); render( <div> <p>_B1</p> <p>_B2</p> <p>_B3</p> <h2>_B4</h2> <p>_B5</p> <p>_B6</p> <h2>_B7</h2> <p>_B8</p> </div>, scratch ); expect(serializeHtml(scratch)).to.equal( '<div><p>_B1</p><p>_B2</p><p>_B3</p><h2>_B4</h2><p>_B5</p><p>_B6</p><h2>_B7</h2><p>_B8</p></div>' ); }); it('should reconcile children in right order #4', () => { render( <div> <p>_A1</p> <p>_A2</p> <div>_A3</div> <h2>_A4</h2> <p>_A5</p> <div>_A6</div> <h2>_A7</h2> <p>_A8</p> <div>_A9</div> <h2>_A10</h2> <p>_A11</p> <div>_A12</div> </div>, scratch ); render( <div> <p>_B1</p> <p>_B2</p> <p>_B3</p> <h2>_B4</h2> <p>_B5</p> <p>_B6</p> <p>_B7</p> <h2>_B8</h2> <p>_B9</p> <p>_B10</p> <p>_B11</p> <p>_B12</p> <h2>_B13</h2> </div>, scratch ); expect(serializeHtml(scratch)).to.equal( '<div><p>_B1</p><p>_B2</p><p>_B3</p><h2>_B4</h2><p>_B5</p><p>_B6</p><p>_B7</p><h2>_B8</h2><p>_B9</p><p>_B10</p><p>_B11</p><p>_B12</p><h2>_B13</h2></div>' ); }); it('should not crash or repeatedly add the same child when replacing a matched vnode with null (mixed dom-types)', () => { const B = () => <div>B</div>; /** @type {() => void} */ let update; class App extends Component { constructor(props) { super(props); this.state = { show: true }; update = () => { this.setState(state => ({ show: !state.show })); }; } render() { if (this.state.show) { return ( <div> <B /> <div>C</div> </div> ); } return ( <div> <span>A</span> {null} <B /> <div>C</div> </div> ); } } render(<App />, scratch); expect(scratch.innerHTML).to.equal('<div><div>B</div><div>C</div></div>'); update(); rerender(); expect(scratch.innerHTML).to.equal( '<div><span>A</span><div>B</div><div>C</div></div>' ); update(); rerender(); expect(scratch.innerHTML).to.equal('<div><div>B</div><div>C</div></div>'); update(); rerender(); expect(scratch.innerHTML).to.equal( '<div><span>A</span><div>B</div><div>C</div></div>' ); }); it('should shrink lists', () => { function RenderedItem({ item }) { if (item.renderAsNullInComponent) { return null; } return <div>{item.id}</div>; } function App({ list }) { return ( <div> {list.map(item => ( <RenderedItem key={item.id} item={item} /> ))} </div> ); } const firstList = [ { id: 'One' }, { id: 'Two' }, { id: 'Three' }, { id: 'Four' } ]; const secondList = [ { id: 'One' }, { id: 'Four', renderAsNullInComponent: true }, { id: 'Six' }, { id: 'Seven' } ]; render(<App list={firstList} />, scratch); expect(scratch.innerHTML).to.equal( '<div><div>One</div><div>Two</div><div>Three</div><div>Four</div></div>' ); render(<App list={secondList} />, scratch); expect(scratch.innerHTML).to.equal( '<div><div>One</div><div>Six</div><div>Seven</div></div>' ); }); it('handles shuffled child-ordering', function () { const App = ({ items }) => ( <div> {items.map(key => ( <div key={key}>{key}</div> ))} </div> ); const a = ['0', '1', '2', '3', '4', '5', '6']; const b = ['1', '3', '5', '2', '6', '4', '0']; const c = ['11', '3', '1', '4', '6', '2', '5', '0', '9', '10']; render(<App items={a} />, scratch); clearLog(); expect(scratch.innerHTML).to.equal( `<div>${a.map(n => `<div>${n}</div>`).join('')}</div>` ); render(<App items={b} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${b.map(n => `<div>${n}</div>`).join('')}</div>` ); expect(getLog()).to.deep.equal([ '<div>0123456.insertBefore(<div>2, <div>6)', '<div>0134526.appendChild(<div>4)', '<div>0135264.appendChild(<div>0)' ]); clearLog(); render(<App items={c} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${c.map(n => `<div>${n}</div>`).join('')}</div>` ); expect(getLog()).to.deep.equal([ '<div>.appendChild(#text)', '<div>1352640.insertBefore(<div>11, <div>1)', '<div>111352640.insertBefore(<div>3, <div>1)', '<div>113152640.insertBefore(<div>4, <div>5)', '<div>113145260.insertBefore(<div>6, <div>5)', '<div>113146520.insertBefore(<div>2, <div>5)', '<div>.appendChild(#text)', '<div>113146250.appendChild(<div>9)', '<div>.appendChild(#text)', '<div>1131462509.appendChild(<div>10)' ]); clearLog(); render(<App items={a} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${a.map(n => `<div>${n}</div>`).join('')}</div>` ); expect(getLog()).to.deep.equal([ '<div>11.remove()', '<div>9.remove()', '<div>10.remove()', '<div>3146250.insertBefore(<div>0, <div>3)', '<div>0314625.insertBefore(<div>1, <div>3)', '<div>0134625.insertBefore(<div>2, <div>3)', '<div>0123465.insertBefore(<div>5, <div>6)' ]); clearLog(); }); it('should shift keyed lists with wrapping fragment-like children', () => { const ItemA = ({ text }) => <div>A: {text}</div>; const ItemB = ({ text }) => <div>B: {text}</div>; const Item = ({ text, type }) => { return type === 'B' ? <ItemB text={text} /> : <ItemA text={text} />; }; let set; class App extends Component { constructor(props) { super(props); this.state = { items: a, mapping: mappingA }; set = (items, mapping) => { this.setState({ items, mapping }); }; } render() { return ( <ul> {this.state.items.map((key, i) => ( <Item key={key} type={this.state.mapping[i]} text={key} /> ))} </ul> ); } } const a = ['4', '1', '2', '3']; const mappingA = ['A', 'A', 'B', 'B']; const b = ['1', '2', '4', '3']; const mappingB = ['B', 'A', 'A', 'A']; const c = ['4', '2', '1', '3']; const mappingC = ['A', 'B', 'B', 'A']; render(<App items={a} mapping={mappingA} />, scratch); expect(scratch.innerHTML).to.equal( '<ul><div>A: 4</div><div>A: 1</div><div>B: 2</div><div>B: 3</div></ul>' ); set(b, mappingB); rerender(); expect(scratch.innerHTML).to.equal( '<ul><div>B: 1</div><div>A: 2</div><div>A: 4</div><div>A: 3</div></ul>' ); set(c, mappingC); rerender(); expect(scratch.innerHTML).to.equal( '<ul><div>A: 4</div><div>B: 2</div><div>B: 1</div><div>A: 3</div></ul>' ); }); it('handle shuffled array children (moving to the front)', () => { const App = ({ items }) => ( <div> {items.map(key => ( <div key={key}>{key}</div> ))} </div> ); const a = ['0', '2', '7', '6', '1', '3', '5', '4']; const b = ['1', '0', '6', '7', '5', '2', '4', '3']; const c = ['0', '7', '2', '1', '3', '5', '6', '4']; render(<App items={a} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${a.map(n => `<div>${n}</div>`).join('')}</div>` ); render(<App items={b} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${b.map(n => `<div>${n}</div>`).join('')}</div>` ); render(<App items={c} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${c.map(n => `<div>${n}</div>`).join('')}</div>` ); }); // #2949 it.skip('should not swap unkeyed chlildren', () => { class X extends Component { constructor(props) { super(props); this.name = props.name; } render() { return <p>{this.name}</p>; } } function Foo({ condition }) { return ( <div> {condition ? '' : <X name="A" />} {condition ? <X name="B" /> : ''} </div> ); } render(<Foo />, scratch); expect(scratch.textContent).to.equal('A'); render(<Foo condition />, scratch); expect(scratch.textContent).to.equal('B'); render(<Foo />, scratch); expect(scratch.textContent).to.equal('A'); }); // #2949 it.skip('should not swap unkeyed chlildren', () => { const calls = []; class X extends Component { constructor(props) { super(props); calls.push(props.name); this.name = props.name; } render() { return <p>{this.name}</p>; } } function Foo({ condition }) { return ( <div> <X name="1" /> {condition ? '' : <X name="A" />} {condition ? <X name="B" /> : ''} <X name="C" /> </div> ); } render(<Foo />, scratch); expect(scratch.textContent).to.equal('1AC'); expect(calls).to.deep.equal(['1', 'A', 'C']); render(<Foo condition />, scratch); expect(scratch.textContent).to.equal('1BC'); expect(calls).to.deep.equal(['1', 'A', 'C', 'B']); render(<Foo />, scratch); expect(scratch.textContent).to.equal('1AC'); expect(calls).to.deep.equal(['1', 'A', 'C', 'B', 'A']); }); it('should retain state for inserted children', () => { class X extends Component { constructor(props) { super(props); this.name = props.name; } render() { return <p>{this.name}</p>; } } function Foo({ condition }) { // We swap the prop from A to B but we don't expect this to // reflect in text-content as we are testing whether the // state is retained for a skew that matches the original children. // // We insert <span /> which should amount to a skew of -1 which should // make us correctly match the X component. return condition ? ( <div> <span /> <X name="B" /> </div> ) : ( <div> <X name="A" /> </div> ); } render(<Foo />, scratch); expect(scratch.textContent).to.equal('A'); render(<Foo condition />, scratch); expect(scratch.textContent).to.equal('A'); render(<Foo />, scratch); expect(scratch.textContent).to.equal('A'); }); it('handle shuffled (stress test)', () => { function randomize(arr) { for (let i = arr.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } const App = ({ items }) => ( <div> {items.map(key => ( <div key={key}>{key}</div> ))} </div> ); const a = Array.from({ length: 8 }).map((_, i) => `${i}`); for (let i = 0; i < 10000; i++) { const aa = randomize(a); render(<App items={aa} />, scratch); expect(scratch.innerHTML).to.equal( `<div>${aa.map(n => `<div>${n}</div>`).join('')}</div>` ); } }); it('should work with document', () => { document.textContent = ''; const App = () => ( <Fragment> <head> <title>Test</title> </head> <body> <p>Test</p> </body> </Fragment> ); render(<App />, document); expect(document.documentElement.innerHTML.trim()).to.equal( '<head><title>Test</title></head><body><p>Test</p></body>' ); }); it('should not remount components when replacing a component with a falsy value in-between', () => { const actions = []; class Comp extends Component { componentDidMount() { actions.push('mounted ' + this.props.i); } render() { return <div>Hello</div>; } } const App = props => { return ( <div> {props.y === '1' ? <Comp i={1} /> : <div />} {false} <Comp i={2} /> <Comp i={3} /> </div> ); }; render(<App y="1" />, scratch); expect(actions).to.deep.equal(['mounted 1', 'mounted 2', 'mounted 3']); render(<App y="2" />, scratch); expect(actions).to.deep.equal(['mounted 1', 'mounted 2', 'mounted 3']); }); it('Should render intercepted component', () => { class Test { constructor(text) { this.text = text; } } const TestValue = props => { return props.text; }; Object.defineProperties(Test.prototype, { constructor: { configurable: true, value: undefined }, type: { configurable: true, value: TestValue }, props: { configurable: true, get() { return { text: this.text }; } }, _depth: { configurable: true, value: 1 } }); const test = new Test('hello world'); const App = () => <Fragment>{test}</Fragment>; render(<App />, scratch); expect(scratch.innerHTML).to.equal('hello world'); }); describe('Alternative document', () => { it('Renders in an iframe', () => { const iframe = document.createElement('iframe'); scratch.appendChild(iframe); const rootCreateElementSpy = vi.spyOn(document, 'createElementNS'); const iframeCreateElementSpy = vi.spyOn( iframe.contentDocument, 'createElementNS' ); const iframeBody = iframe.contentDocument.getElementsByTagName('body')[0]; render(<div>Hello world</div>, iframeBody); expect(iframe.contentDocument.body.innerHTML).to.equal( '<div>Hello world</div>' ); expect(rootCreateElementSpy).not.toBeCalled(); expect(iframeCreateElementSpy).toBeCalled(); }); }); });