import { createElement, Component, render, createRef } from 'preact';
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../_util/helpers';
import { logCall, clearLog, getLog } from '../_util/logCall';
import { div } from '../_util/dom';
/** @jsx createElement */
describe('null placeholders', () => {
/** @type {HTMLDivElement} */
let scratch;
/** @type {() => void} */
let rerender;
/** @type {string[]} */
let ops;
function createNullable(name) {
return function Nullable(props) {
return props.show ? name : null;
};
}
/**
* @param {string} name
* @returns {[import('preact').ComponentClass, import('preact').RefObject<{ toggle(): void }>]}
*/
function createStatefulNullable(name) {
let ref = createRef();
class Nullable extends Component {
constructor(props) {
super(props);
this.state = { show: props.initialShow || true };
ref.current = this;
}
toggle() {
this.setState({ show: !this.state.show });
}
componentDidUpdate() {
ops.push(`Update ${name}`);
}
componentDidMount() {
ops.push(`Mount ${name}`);
}
componentWillUnmount() {
ops.push(`Unmount ${name}`);
}
render() {
return this.state.show ?
bar.remove()']);
});
it('should preserve state of Components when using null or booleans as placeholders', () => {
// Must be the same class for all children in
for this test to be valid
class Stateful extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment() {
this.setState({ count: this.state.count + 1 });
}
componentDidUpdate() {
ops.push(`Update ${this.props.name}`);
}
componentDidMount() {
ops.push(`Mount ${this.props.name}`);
}
componentWillUnmount() {
ops.push(`Unmount ${this.props.name}`);
}
render() {
return (
{this.props.name}: {this.state.count}
);
}
}
const s1ref = createRef();
const s2ref = createRef();
const s3ref = createRef();
function App({ first = null, second = false }) {
return [first, second,
];
}
// Mount third stateful - Initial render
render(
, scratch);
expect(scratch.innerHTML).to.equal('
third: 0
');
expect(ops).to.deep.equal(['Mount third'], 'mount third');
// Update third stateful
ops = [];
s3ref.current.increment();
rerender();
expect(scratch.innerHTML).to.equal('
third: 1
');
expect(ops).to.deep.equal(['Update third'], 'update third');
// Mount first stateful
ops = [];
render(
} />, scratch);
expect(scratch.innerHTML).to.equal(
'
first: 0
third: 1
'
);
expect(ops).to.deep.equal(['Mount first', 'Update third'], 'mount first');
// Update first stateful
ops = [];
s1ref.current.increment();
s3ref.current.increment();
rerender();
expect(scratch.innerHTML).to.equal(
'
first: 1
third: 2
'
);
expect(ops).to.deep.equal(['Update first', 'Update third'], 'update first');
// Mount second stateful
ops = [];
render(
}
second={
}
/>,
scratch
);
expect(scratch.innerHTML).to.equal(
'
first: 1
second: 0
third: 2
'
);
expect(ops).to.deep.equal(
['Update first', 'Mount second', 'Update third'],
'mount second'
);
// Update second stateful
ops = [];
s1ref.current.increment();
s2ref.current.increment();
s3ref.current.increment();
rerender();
expect(scratch.innerHTML).to.equal(
'
first: 2
second: 1
third: 3
'
);
expect(ops).to.deep.equal(
['Update first', 'Update second', 'Update third'],
'update second'
);
});
it('should efficiently replace self-updating null placeholders', () => {
// These Nullable components replace themselves with null without the parent re-rendering
const [Nullable, ref] = createStatefulNullable('Nullable');
const [Nullable2, ref2] = createStatefulNullable('Nullable2');
function App() {
return (
);
}
render(
, scratch);
expect(scratch.innerHTML).to.equal(
div([div(1), div('Nullable'), div(3), div('Nullable2')])
);
clearLog();
ref2.current.toggle();
ref.current.toggle();
rerender();
expect(scratch.innerHTML).to.equal(div([div(1), div(3)]));
expect(getLog()).to.deep.equal([
'
Nullable2.remove()',
'
Nullable.remove()'
]);
clearLog();
ref2.current.toggle();
ref.current.toggle();
rerender();
expect(scratch.innerHTML).to.equal(
div([div(1), div('Nullable'), div(3), div('Nullable2')])
);
expect(getLog()).to.deep.equal([
'
.appendChild(#text)',
'
13.appendChild(
Nullable2)',
'
.appendChild(#text)',
'
13Nullable2.insertBefore(
Nullable,
3)'
]);
});
// See preactjs/preact#2350
it('should efficiently replace null placeholders in parent rerenders (#2350)', () => {
// This Nullable only changes when it's parent rerenders
const Nullable1 = createNullable('Nullable 1');
const Nullable2 = createNullable('Nullable 2');
/** @type {() => void} */
let toggle;
class App extends Component {
constructor(props) {
super(props);
this.state = { show: false };
toggle = () => this.setState({ show: !this.state.show });
}
render() {
return (
{this.state.show.toString()}
the middle
);
}
}
render(
, scratch);
expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')]));
clearLog();
toggle();
rerender();
expect(scratch.innerHTML).to.equal(
div([div('true'), 'Nullable 1', div('the middle'), 'Nullable 2'])
);
expect(getLog()).to.deep.equal([
'
truethe middle.insertBefore(#text,
the middle)',
'
trueNullable 1the middle.appendChild(#text)'
]);
clearLog();
toggle();
rerender();
expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')]));
expect(getLog()).to.deep.equal([
'#text.remove()',
// '
falsethe middleNullable 2.appendChild(
the middle)',
'#text.remove()'
]);
});
});