Testing Node Replacement and Component Lifecycle Management in Preact
This test suite validates the replaceNode parameter functionality in Preact’s render method, focusing on DOM node replacement and component lifecycle management. It ensures proper handling of node replacement, sibling preservation, and component unmounting scenarios.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
preactjs/preact
test/browser/replaceNode.test.js
import { createElement, render, Component } from 'preact';
import {
setupScratch,
teardown,
serializeHtml,
sortAttributes
} from '../_util/helpers';
/** @jsx createElement */
describe('replaceNode parameter in render()', () => {
let scratch;
/**
* @param {HTMLDivElement} container
* @returns {HTMLDivElement[]}
*/
function setupABCDom(container) {
return ['a', 'b', 'c'].map(id => {
const child = document.createElement('div');
child.id = id;
container.appendChild(child);
return child;
});
}
beforeEach(() => {
scratch = setupScratch();
});
afterEach(() => {
teardown(scratch);
});
it('should use replaceNode as render root and not inject into it', () => {
setupABCDom(scratch);
const childA = scratch.querySelector('#a');
render(<div id="a">contents</div>, scratch, childA);
expect(scratch.querySelector('#a')).to.equalNode(childA);
expect(childA.innerHTML).to.equal('contents');
});
it('should not remove siblings of replaceNode', () => {
setupABCDom(scratch);
const childA = scratch.querySelector('#a');
render(<div id="a" />, scratch, childA);
expect(scratch.innerHTML).to.equal(
'<div id="a"></div><div id="b"></div><div id="c"></div>'
);
});
it('should notice prop changes on replaceNode', () => {
setupABCDom(scratch);
const childA = scratch.querySelector('#a');
render(<div id="a" className="b" />, scratch, childA);
expect(sortAttributes(String(scratch.innerHTML))).to.equal(
sortAttributes(
'<div id="a" class="b"></div><div id="b"></div><div id="c"></div>'
)
);
});
it('should unmount existing components', () => {
const unmount = sinon.spy();
const mount = sinon.spy();
class App extends Component {
componentDidMount() {
mount();
}
componentWillUnmount() {
unmount();
}
render() {
return <div>App</div>;
}
}
render(
<div id="a">
<App />
</div>,
scratch
);
expect(scratch.innerHTML).to.equal('<div id="a"><div>App</div></div>');
expect(mount).to.be.calledOnce;
render(<div id="a">new</div>, scratch, scratch.querySelector('#a'));
expect(scratch.innerHTML).to.equal('<div id="a">new</div>');
expect(unmount).to.be.calledOnce;
});
it('should unmount existing components in prerendered HTML', () => {
const unmount = sinon.spy();
const mount = sinon.spy();
class App extends Component {
componentDidMount() {
mount();
}
componentWillUnmount() {
unmount();
}
render() {
return <span>App</span>;
}
}
scratch.innerHTML = `<div id="child"></div>`;
const childContainer = scratch.querySelector('#child');
render(<App />, childContainer);
expect(serializeHtml(childContainer)).to.equal('<span>App</span>');
expect(mount).to.be.calledOnce;
render(<div />, scratch, scratch.firstElementChild);
expect(serializeHtml(scratch)).to.equal('<div id=""></div>');
expect(unmount).to.be.calledOnce;
});
it('should render multiple render roots in one parentDom', () => {
setupABCDom(scratch);
const childA = scratch.querySelector('#a');
const childB = scratch.querySelector('#b');
const childC = scratch.querySelector('#c');
const expectedA = '<div id="a">childA</div>';
const expectedB = '<div id="b">childB</div>';
const expectedC = '<div id="c">childC</div>';
render(<div id="a">childA</div>, scratch, childA);
render(<div id="b">childB</div>, scratch, childB);
render(<div id="c">childC</div>, scratch, childC);
expect(scratch.innerHTML).to.equal(`${expectedA}${expectedB}${expectedC}`);
});
it('should call unmount when working with replaceNode', () => {
const mountSpy = sinon.spy();
const unmountSpy = sinon.spy();
class MyComponent extends Component {
componentDidMount() {
mountSpy();
}
componentWillUnmount() {
unmountSpy();
}
render() {
return <div>My Component</div>;
}
}
const container = document.createElement('div');
scratch.appendChild(container);
render(<MyComponent />, scratch, container);
expect(mountSpy).to.be.calledOnce;
render(<div>Not my component</div>, document.body, container);
expect(unmountSpy).to.be.calledOnce;
});
it('should double replace', () => {
const container = document.createElement('div');
scratch.appendChild(container);
render(<div>Hello</div>, scratch, scratch.firstElementChild);
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
render(<div>Hello</div>, scratch, scratch.firstElementChild);
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
});
it('should replaceNode after rendering', () => {
function App({ i }) {
return <p>{i}</p>;
}
render(<App i={2} />, scratch);
expect(scratch.innerHTML).to.equal('<p>2</p>');
render(<App i={3} />, scratch, scratch.firstChild);
expect(scratch.innerHTML).to.equal('<p>3</p>');
});
it("shouldn't remove elements on subsequent renders with replaceNode", () => {
const placeholder = document.createElement('div');
scratch.appendChild(placeholder);
const App = () => (
<div>
New content
<button>Update</button>
</div>
);
render(<App />, scratch, placeholder);
expect(scratch.innerHTML).to.equal(
'<div>New content<button>Update</button></div>'
);
render(<App />, scratch, placeholder);
expect(scratch.innerHTML).to.equal(
'<div>New content<button>Update</button></div>'
);
});
it('should remove redundant elements on subsequent renders with replaceNode', () => {
const placeholder = document.createElement('div');
scratch.appendChild(placeholder);
const App = () => (
<div>
New content
<button>Update</button>
</div>
);
render(<App />, scratch, placeholder);
expect(scratch.innerHTML).to.equal(
'<div>New content<button>Update</button></div>'
);
placeholder.appendChild(document.createElement('span'));
expect(scratch.innerHTML).to.equal(
'<div>New content<button>Update</button><span></span></div>'
);
render(<App />, scratch, placeholder);
expect(scratch.innerHTML).to.equal(
'<div>New content<button>Update</button></div>'
);
});
});