From 30b577138dda685f65a8529be1866afa6e321845 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 4 Oct 2016 11:50:26 +0200 Subject: Squashed 'thirdparty/preact/' content from commit b2d90cc git-subtree-dir: thirdparty/preact git-subtree-split: b2d90cc116f1d1998f7a7c98dc6986bf4c1841f4 --- test/browser/components.js | 713 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 test/browser/components.js (limited to 'test/browser/components.js') diff --git a/test/browser/components.js b/test/browser/components.js new file mode 100644 index 000000000..b4649a719 --- /dev/null +++ b/test/browser/components.js @@ -0,0 +1,713 @@ +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 = '>'; + 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.calledWith(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.calledWith(PROPS) + .and.to.have.returned(sinon.match({ + nodeName: Inner, + attributes: PROPS + })); + + expect(Inner) + .to.have.been.calledOnce + .and.to.have.been.calledWith(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.calledWith({ 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.calledWith({ 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.calledWith({ 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.calledWith({ 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); + + root = 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; + // @TODO this was just incorrect? + // 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; + }); + }); +}); -- cgit v1.2.3