import { h, render, rerender, Component } from '../../src/preact';
/** @jsx h */
let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) );
function getAttributes(node) {
let attrs = {};
if (node.attributes) {
for (let i=node.attributes.length; i--; ) {
attrs[node.attributes[i].name] = node.attributes[i].value;
}
}
return attrs;
}
// hacky normalization of attribute order across browsers.
function sortAttributes(html) {
return html.replace(/<([a-z0-9-]+)((?:\s[a-z0-9:_.-]+=".*?")+)((?:\s*\/)?>)/gi, (s, pre, attrs, after) => {
let list = attrs.match(/\s[a-z0-9:_.-]+=".*?"/gi).sort( (a, b) => a>b ? 1 : -1 );
if (~after.indexOf('/')) after = '>'+pre+'>';
return '<' + pre + list.join('') + after;
});
}
const Empty = () => null;
describe('Components', () => {
let scratch;
before( () => {
scratch = document.createElement('div');
(document.body || document.documentElement).appendChild(scratch);
});
beforeEach( () => {
let c = scratch.firstElementChild;
if (c) render(, scratch, c);
scratch.innerHTML = '';
});
after( () => {
scratch.parentNode.removeChild(scratch);
scratch = null;
});
it('should render components', () => {
class C1 extends Component {
render() {
return
C1
;
}
}
sinon.spy(C1.prototype, 'render');
render(, scratch);
expect(C1.prototype.render)
.to.have.been.calledOnce
.and.to.have.been.calledWithMatch({}, {})
.and.to.have.returned(sinon.match({ nodeName:'div' }));
expect(scratch.innerHTML).to.equal('C1
');
});
it('should render functional components', () => {
const PROPS = { foo:'bar', onBaz:()=>{} };
const C3 = sinon.spy( props => );
render(, scratch);
expect(C3)
.to.have.been.calledOnce
.and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({
nodeName: 'div',
attributes: PROPS
}));
expect(scratch.innerHTML).to.equal('');
});
it('should render components with props', () => {
const PROPS = { foo:'bar', onBaz:()=>{} };
let constructorProps;
class C2 extends Component {
constructor(props) {
super(props);
constructorProps = props;
}
render(props) {
return ;
}
}
sinon.spy(C2.prototype, 'render');
render(, scratch);
expect(constructorProps).to.deep.equal(PROPS);
expect(C2.prototype.render)
.to.have.been.calledOnce
.and.to.have.been.calledWithMatch(PROPS, {})
.and.to.have.returned(sinon.match({
nodeName: 'div',
attributes: PROPS
}));
expect(scratch.innerHTML).to.equal('');
});
// Test for Issue #73
it('should remove orphaned elements replaced by Components', () => {
class Comp extends Component {
render() {
return span in a component;
}
}
let root;
function test(content) {
root = render(content, scratch, root);
}
test();
test(just a div
);
test();
expect(scratch.innerHTML).to.equal('span in a component');
});
// Test for Issue #176
it('should remove children when root changes to text node', () => {
let comp;
class Comp extends Component {
render(_, { alt }) {
return alt ? 'asdf' : test
;
}
}
render(comp=c} />, scratch);
comp.setState({ alt:true });
comp.forceUpdate();
expect(scratch.innerHTML, 'switching to textnode').to.equal('asdf');
comp.setState({ alt:false });
comp.forceUpdate();
expect(scratch.innerHTML, 'switching to element').to.equal('test
');
comp.setState({ alt:true });
comp.forceUpdate();
expect(scratch.innerHTML, 'switching to textnode 2').to.equal('asdf');
});
describe('props.children', () => {
it('should support passing children as a prop', () => {
const Foo = props => ;
render(bar,
'123',
456
]} />, scratch);
expect(scratch.innerHTML).to.equal('bar123456
');
});
it('should be ignored when explicit children exist', () => {
const Foo = props => a
;
render(, scratch);
expect(scratch.innerHTML).to.equal('a
');
});
});
describe('High-Order Components', () => {
it('should render nested functional components', () => {
const PROPS = { foo:'bar', onBaz:()=>{} };
const Outer = sinon.spy(
props =>
);
const Inner = sinon.spy(
props => inner
);
render(, scratch);
expect(Outer)
.to.have.been.calledOnce
.and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({
nodeName: Inner,
attributes: PROPS
}));
expect(Inner)
.to.have.been.calledOnce
.and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({
nodeName: 'div',
attributes: PROPS,
children: ['inner']
}));
expect(scratch.innerHTML).to.equal('inner
');
});
it('should re-render nested functional components', () => {
let doRender = null;
class Outer extends Component {
componentDidMount() {
let i = 1;
doRender = () => this.setState({ i: ++i });
}
componentWillUnmount() {}
render(props, { i }) {
return ;
}
}
sinon.spy(Outer.prototype, 'render');
sinon.spy(Outer.prototype, 'componentWillUnmount');
let j = 0;
const Inner = sinon.spy(
props => inner
);
render(, scratch);
// update & flush
doRender();
rerender();
expect(Outer.prototype.componentWillUnmount)
.not.to.have.been.called;
expect(Inner).to.have.been.calledTwice;
expect(Inner.secondCall)
.to.have.been.calledWithMatch({ foo:'bar', i:2 })
.and.to.have.returned(sinon.match({
attributes: {
j: 2,
i: 2,
foo: 'bar'
}
}));
expect(getAttributes(scratch.firstElementChild)).to.eql({
j: '2',
i: '2',
foo: 'bar'
});
// update & flush
doRender();
rerender();
expect(Inner).to.have.been.calledThrice;
expect(Inner.thirdCall)
.to.have.been.calledWithMatch({ foo:'bar', i:3 })
.and.to.have.returned(sinon.match({
attributes: {
j: 3,
i: 3,
foo: 'bar'
}
}));
expect(getAttributes(scratch.firstElementChild)).to.eql({
j: '3',
i: '3',
foo: 'bar'
});
});
it('should re-render nested components', () => {
let doRender = null,
alt = false;
class Outer extends Component {
componentDidMount() {
let i = 1;
doRender = () => this.setState({ i: ++i });
}
componentWillUnmount() {}
render(props, { i }) {
if (alt) return ;
return ;
}
}
sinon.spy(Outer.prototype, 'render');
sinon.spy(Outer.prototype, 'componentDidMount');
sinon.spy(Outer.prototype, 'componentWillUnmount');
let j = 0;
class Inner extends Component {
constructor(...args) {
super();
this._constructor(...args);
}
_constructor() {}
componentWillMount() {}
componentDidMount() {}
componentWillUnmount() {}
componentDidUnmount() {}
render(props) {
return inner
;
}
}
sinon.spy(Inner.prototype, '_constructor');
sinon.spy(Inner.prototype, 'render');
sinon.spy(Inner.prototype, 'componentWillMount');
sinon.spy(Inner.prototype, 'componentDidMount');
sinon.spy(Inner.prototype, 'componentDidUnmount');
sinon.spy(Inner.prototype, 'componentWillUnmount');
render(, scratch);
expect(Outer.prototype.componentDidMount).to.have.been.calledOnce;
// update & flush
doRender();
rerender();
expect(Outer.prototype.componentWillUnmount).not.to.have.been.called;
expect(Inner.prototype._constructor).to.have.been.calledOnce;
expect(Inner.prototype.componentWillUnmount).not.to.have.been.called;
expect(Inner.prototype.componentDidUnmount).not.to.have.been.called;
expect(Inner.prototype.componentWillMount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
expect(Inner.prototype.render).to.have.been.calledTwice;
expect(Inner.prototype.render.secondCall)
.to.have.been.calledWithMatch({ foo:'bar', i:2 })
.and.to.have.returned(sinon.match({
attributes: {
j: 2,
i: 2,
foo: 'bar'
}
}));
expect(getAttributes(scratch.firstElementChild)).to.eql({
j: '2',
i: '2',
foo: 'bar'
});
expect(sortAttributes(scratch.innerHTML)).to.equal(sortAttributes('inner
'));
// update & flush
doRender();
rerender();
expect(Inner.prototype.componentWillUnmount).not.to.have.been.called;
expect(Inner.prototype.componentDidUnmount).not.to.have.been.called;
expect(Inner.prototype.componentWillMount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
expect(Inner.prototype.render).to.have.been.calledThrice;
expect(Inner.prototype.render.thirdCall)
.to.have.been.calledWithMatch({ foo:'bar', i:3 })
.and.to.have.returned(sinon.match({
attributes: {
j: 3,
i: 3,
foo: 'bar'
}
}));
expect(getAttributes(scratch.firstElementChild)).to.eql({
j: '3',
i: '3',
foo: 'bar'
});
// update & flush
alt = true;
doRender();
rerender();
expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce;
expect(scratch.innerHTML).to.equal('');
// update & flush
alt = false;
doRender();
rerender();
expect(sortAttributes(scratch.innerHTML)).to.equal(sortAttributes('inner
'));
});
it('should resolve intermediary functional component', () => {
let ctx = {};
class Root extends Component {
getChildContext() {
return { ctx };
}
render() {
return ;
}
}
const Func = sinon.spy( () => );
class Inner extends Component {
componentWillMount() {}
componentDidMount() {}
componentWillUnmount() {}
componentDidUnmount() {}
render() {
return inner
;
}
}
spyAll(Inner.prototype);
let root = render(, scratch);
expect(Inner.prototype.componentWillMount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
expect(Inner.prototype.componentWillMount).to.have.been.calledBefore(Inner.prototype.componentDidMount);
render(, scratch, root);
expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce;
expect(Inner.prototype.componentWillUnmount).to.have.been.calledBefore(Inner.prototype.componentDidUnmount);
});
it('should unmount children of high-order components without unmounting parent', () => {
let outer, inner2, counter=0;
class Outer extends Component {
constructor(props, context) {
super(props, context);
outer = this;
this.state = {
child: this.props.child
};
}
componentWillUnmount(){}
componentDidUnmount(){}
componentWillMount(){}
componentDidMount(){}
render(_, { child:C }) {
return ;
}
}
spyAll(Outer.prototype);
class Inner extends Component {
componentWillUnmount(){}
componentDidUnmount(){}
componentWillMount(){}
componentDidMount(){}
render() {
return h('element'+(++counter));
}
}
spyAll(Inner.prototype);
class Inner2 extends Component {
constructor(props, context) {
super(props, context);
inner2 = this;
}
componentWillUnmount(){}
componentDidUnmount(){}
componentWillMount(){}
componentDidMount(){}
render() {
return h('element'+(++counter));
}
}
spyAll(Inner2.prototype);
render(, scratch);
// outer should only have been mounted once
expect(Outer.prototype.componentWillMount, 'outer initial').to.have.been.calledOnce;
expect(Outer.prototype.componentDidMount, 'outer initial').to.have.been.calledOnce;
expect(Outer.prototype.componentWillUnmount, 'outer initial').not.to.have.been.called;
expect(Outer.prototype.componentDidUnmount, 'outer initial').not.to.have.been.called;
// inner should only have been mounted once
expect(Inner.prototype.componentWillMount, 'inner initial').to.have.been.calledOnce;
expect(Inner.prototype.componentDidMount, 'inner initial').to.have.been.calledOnce;
expect(Inner.prototype.componentWillUnmount, 'inner initial').not.to.have.been.called;
expect(Inner.prototype.componentDidUnmount, 'inner initial').not.to.have.been.called;
outer.setState({ child:Inner2 });
outer.forceUpdate();
expect(Inner2.prototype.render).to.have.been.calledOnce;
// outer should still only have been mounted once
expect(Outer.prototype.componentWillMount, 'outer swap').to.have.been.calledOnce;
expect(Outer.prototype.componentDidMount, 'outer swap').to.have.been.calledOnce;
expect(Outer.prototype.componentWillUnmount, 'outer swap').not.to.have.been.called;
expect(Outer.prototype.componentDidUnmount, 'outer swap').not.to.have.been.called;
// inner should only have been mounted once
expect(Inner2.prototype.componentWillMount, 'inner2 swap').to.have.been.calledOnce;
expect(Inner2.prototype.componentDidMount, 'inner2 swap').to.have.been.calledOnce;
expect(Inner2.prototype.componentWillUnmount, 'inner2 swap').not.to.have.been.called;
expect(Inner2.prototype.componentDidUnmount, 'inner2 swap').not.to.have.been.called;
inner2.forceUpdate();
expect(Inner2.prototype.render, 'inner2 update').to.have.been.calledTwice;
expect(Inner2.prototype.componentWillMount, 'inner2 update').to.have.been.calledOnce;
expect(Inner2.prototype.componentDidMount, 'inner2 update').to.have.been.calledOnce;
expect(Inner2.prototype.componentWillUnmount, 'inner2 update').not.to.have.been.called;
expect(Inner2.prototype.componentDidUnmount, 'inner2 update').not.to.have.been.called;
});
it('should remount when swapping between HOC child types', () => {
class Outer extends Component {
render({ child: Child }) {
return ;
}
}
class Inner extends Component {
componentWillMount() {}
componentWillUnmount() {}
render() {
return foo
;
}
}
spyAll(Inner.prototype);
const InnerFunc = () => (
bar
);
let root = render(, scratch, root);
expect(Inner.prototype.componentWillMount, 'initial mount').to.have.been.calledOnce;
expect(Inner.prototype.componentWillUnmount, 'initial mount').not.to.have.been.called;
Inner.prototype.componentWillMount.reset();
root = render(, scratch, root);
expect(Inner.prototype.componentWillMount, 'unmount').not.to.have.been.called;
expect(Inner.prototype.componentWillUnmount, 'unmount').to.have.been.calledOnce;
Inner.prototype.componentWillUnmount.reset();
root = render(, scratch, root);
expect(Inner.prototype.componentWillMount, 'remount').to.have.been.calledOnce;
expect(Inner.prototype.componentWillUnmount, 'remount').not.to.have.been.called;
});
});
describe('Component Nesting', () => {
let useIntermediary = false;
let createComponent = (Intermediary) => {
class C extends Component {
componentWillMount() {}
componentDidUnmount() {}
render({ children }) {
if (!useIntermediary) return children[0];
let I = useIntermediary===true ? Intermediary : useIntermediary;
return {children};
}
}
spyAll(C.prototype);
return C;
};
let createFunction = () => sinon.spy( ({ children }) => children[0] );
let root;
let rndr = n => root = render(n, scratch, root);
let F1 = createFunction();
let F2 = createFunction();
let F3 = createFunction();
let C1 = createComponent(F1);
let C2 = createComponent(F2);
let C3 = createComponent(F3);
let reset = () => [C1, C2, C3].reduce(
(acc, c) => acc.concat( Object.keys(c.prototype).map(key => c.prototype[key]) ),
[F1, F2, F3]
).forEach( c => c.reset && c.reset() );
it('should handle lifecycle for no intermediary in component tree', () => {
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'initial mount').to.have.been.calledOnce;
expect(C2.prototype.componentWillMount, 'initial mount').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'initial mount').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'unmount innermost, C1').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'unmount innermost, C2').not.to.have.been.called;
expect(C3.prototype.componentDidUnmount, 'unmount innermost, C3').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'swap innermost').not.to.have.been.called;
expect(C2.prototype.componentDidUnmount, 'swap innermost').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'swap innermost').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentDidUnmount, 'inject between, C1').not.to.have.been.called;
expect(C1.prototype.componentWillMount, 'inject between, C1').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'inject between, C2').to.have.been.calledOnce;
expect(C3.prototype.componentDidUnmount, 'inject between, C3').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'inject between, C3').to.have.been.calledOnce;
});
it('should handle lifecycle for nested intermediary functional components', () => {
useIntermediary = true;
rndr();
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'initial mount w/ intermediary fn, C1').to.have.been.calledOnce;
expect(C2.prototype.componentWillMount, 'initial mount w/ intermediary fn, C2').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'initial mount w/ intermediary fn, C3').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'unmount innermost w/ intermediary fn, C1').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'unmount innermost w/ intermediary fn, C2').not.to.have.been.called;
expect(C3.prototype.componentDidUnmount, 'unmount innermost w/ intermediary fn, C3').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'swap innermost w/ intermediary fn').not.to.have.been.called;
expect(C2.prototype.componentDidUnmount, 'swap innermost w/ intermediary fn').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'swap innermost w/ intermediary fn').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentDidUnmount, 'inject between, C1 w/ intermediary fn').not.to.have.been.called;
expect(C1.prototype.componentWillMount, 'inject between, C1 w/ intermediary fn').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'inject between, C2 w/ intermediary fn').to.have.been.calledOnce;
expect(C3.prototype.componentDidUnmount, 'inject between, C3 w/ intermediary fn').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'inject between, C3 w/ intermediary fn').to.have.been.calledOnce;
});
it('should handle lifecycle for nested intermediary elements', () => {
useIntermediary = 'div';
rndr();
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'initial mount w/ intermediary div, C1').to.have.been.calledOnce;
expect(C2.prototype.componentWillMount, 'initial mount w/ intermediary div, C2').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'initial mount w/ intermediary div, C3').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C1').not.to.have.been.called;
expect(C2.prototype.componentDidUnmount, 'unmount innermost w/ intermediary div, C2 ummount').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C2').not.to.have.been.called;
expect(C3.prototype.componentDidUnmount, 'unmount innermost w/ intermediary div, C3').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentWillMount, 'swap innermost w/ intermediary div').not.to.have.been.called;
expect(C2.prototype.componentDidUnmount, 'swap innermost w/ intermediary div').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'swap innermost w/ intermediary div').to.have.been.calledOnce;
reset();
rndr(Some Text);
expect(C1.prototype.componentDidUnmount, 'inject between, C1 w/ intermediary div').not.to.have.been.called;
expect(C1.prototype.componentWillMount, 'inject between, C1 w/ intermediary div').not.to.have.been.called;
expect(C2.prototype.componentWillMount, 'inject between, C2 w/ intermediary div').to.have.been.calledOnce;
expect(C3.prototype.componentDidUnmount, 'inject between, C3 w/ intermediary div').to.have.been.calledOnce;
expect(C3.prototype.componentWillMount, 'inject between, C3 w/ intermediary div').to.have.been.calledOnce;
});
});
});