wallet-core/thirdparty/preact/test/browser/components.js
2016-11-08 15:19:39 +01:00

713 lines
22 KiB
JavaScript

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(<Empty />, scratch, c);
scratch.innerHTML = '';
});
after( () => {
scratch.parentNode.removeChild(scratch);
scratch = null;
});
it('should render components', () => {
class C1 extends Component {
render() {
return <div>C1</div>;
}
}
sinon.spy(C1.prototype, 'render');
render(<C1 />, 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('<div>C1</div>');
});
it('should render functional components', () => {
const PROPS = { foo:'bar', onBaz:()=>{} };
const C3 = sinon.spy( props => <div {...props} /> );
render(<C3 {...PROPS} />, 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('<div foo="bar"></div>');
});
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 <div {...props} />;
}
}
sinon.spy(C2.prototype, 'render');
render(<C2 {...PROPS} />, 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('<div foo="bar"></div>');
});
// Test for Issue #73
it('should remove orphaned elements replaced by Components', () => {
class Comp extends Component {
render() {
return <span>span in a component</span>;
}
}
let root;
function test(content) {
root = render(content, scratch, root);
}
test(<Comp />);
test(<div>just a div</div>);
test(<Comp />);
expect(scratch.innerHTML).to.equal('<span>span in a component</span>');
});
// 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' : <div>test</div>;
}
}
render(<Comp ref={c=>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('<div>test</div>');
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 => <div {...props} />;
render(<Foo a="b" children={[
<span class="bar">bar</span>,
'123',
456
]} />, scratch);
expect(scratch.innerHTML).to.equal('<div a="b"><span class="bar">bar</span>123456</div>');
});
it('should be ignored when explicit children exist', () => {
const Foo = props => <div {...props}>a</div>;
render(<Foo children={'b'} />, scratch);
expect(scratch.innerHTML).to.equal('<div>a</div>');
});
});
describe('High-Order Components', () => {
it('should render nested functional components', () => {
const PROPS = { foo:'bar', onBaz:()=>{} };
const Outer = sinon.spy(
props => <Inner {...props} />
);
const Inner = sinon.spy(
props => <div {...props}>inner</div>
);
render(<Outer {...PROPS} />, 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('<div foo="bar">inner</div>');
});
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 <Inner i={i} {...props} />;
}
}
sinon.spy(Outer.prototype, 'render');
sinon.spy(Outer.prototype, 'componentWillUnmount');
let j = 0;
const Inner = sinon.spy(
props => <div j={ ++j } {...props}>inner</div>
);
render(<Outer foo="bar" />, 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 <div is-alt />;
return <Inner i={i} {...props} />;
}
}
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 <div j={ ++j } {...props}>inner</div>;
}
}
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(<Outer foo="bar" />, 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('<div foo="bar" j="2" i="2">inner</div>'));
// 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('<div is-alt="true"></div>');
// update & flush
alt = false;
doRender();
rerender();
expect(sortAttributes(scratch.innerHTML)).to.equal(sortAttributes('<div foo="bar" j="4" i="5">inner</div>'));
});
it('should resolve intermediary functional component', () => {
let ctx = {};
class Root extends Component {
getChildContext() {
return { ctx };
}
render() {
return <Func />;
}
}
const Func = sinon.spy( () => <Inner /> );
class Inner extends Component {
componentWillMount() {}
componentDidMount() {}
componentWillUnmount() {}
componentDidUnmount() {}
render() {
return <div>inner</div>;
}
}
spyAll(Inner.prototype);
let root = render(<Root />, 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(<asdf />, 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 <C />;
}
}
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(<Outer child={Inner} />, 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 <Child />;
}
}
class Inner extends Component {
componentWillMount() {}
componentWillUnmount() {}
render() {
return <div class="inner">foo</div>;
}
}
spyAll(Inner.prototype);
const InnerFunc = () => (
<div class="inner-func">bar</div>
);
let root = render(<Outer child={Inner} />, 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(<Outer child={InnerFunc} />, 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(<Outer child={Inner} />, 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 <I>{children}</I>;
}
}
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(<C1><C2><C3>Some Text</C3></C2></C1>);
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(<C1><C2>Some Text</C2></C1>);
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(<C1><C3>Some Text</C3></C1>);
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(<C1><C2><C3>Some Text</C3></C2></C1>);
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(<div />);
reset();
rndr(<C1><C2><C3>Some Text</C3></C2></C1>);
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(<C1><C2>Some Text</C2></C1>);
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(<C1><C3>Some Text</C3></C1>);
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(<C1><C2><C3>Some Text</C3></C2></C1>);
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(<div />);
reset();
rndr(<C1><C2><C3>Some Text</C3></C2></C1>);
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(<C1><C2>Some Text</C2></C1>);
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(<C1><C3>Some Text</C3></C1>);
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(<C1><C2><C3>Some Text</C3></C2></C1>);
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;
});
});
});