diff options
Diffstat (limited to 'thirdparty/preact/test/browser')
-rw-r--r-- | thirdparty/preact/test/browser/components.js | 713 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/context.js | 170 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/keys.js | 85 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/lifecycle.js | 493 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/linked-state.js | 98 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/performance.js | 245 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/refs.js | 287 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/render.js | 439 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/spec.js | 124 | ||||
-rw-r--r-- | thirdparty/preact/test/browser/svg.js | 112 |
10 files changed, 2766 insertions, 0 deletions
diff --git a/thirdparty/preact/test/browser/components.js b/thirdparty/preact/test/browser/components.js new file mode 100644 index 000000000..b4649a719 --- /dev/null +++ b/thirdparty/preact/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 = '></'+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.calledWith(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.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('<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.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 <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.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('<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.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('<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); + + root = 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; + // @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(<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; + }); + }); +}); diff --git a/thirdparty/preact/test/browser/context.js b/thirdparty/preact/test/browser/context.js new file mode 100644 index 000000000..e62a948a4 --- /dev/null +++ b/thirdparty/preact/test/browser/context.js @@ -0,0 +1,170 @@ +import { h, render, Component } from '../../src/preact'; +/** @jsx h */ + +describe('context', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + it('should pass context to grandchildren', () => { + const CONTEXT = { a:'a' }; + const PROPS = { b:'b' }; + // let inner; + + class Outer extends Component { + getChildContext() { + return CONTEXT; + } + render(props) { + return <div><Inner {...props} /></div>; + } + } + sinon.spy(Outer.prototype, 'getChildContext'); + + class Inner extends Component { + // constructor() { + // super(); + // inner = this; + // } + shouldComponentUpdate() { return true; } + componentWillReceiveProps() {} + componentWillUpdate() {} + componentDidUpdate() {} + render(props, state, context) { + return <div>{ context && context.a }</div>; + } + } + sinon.spy(Inner.prototype, 'shouldComponentUpdate'); + sinon.spy(Inner.prototype, 'componentWillReceiveProps'); + sinon.spy(Inner.prototype, 'componentWillUpdate'); + sinon.spy(Inner.prototype, 'componentDidUpdate'); + sinon.spy(Inner.prototype, 'render'); + + render(<Outer />, scratch, scratch.lastChild); + + expect(Outer.prototype.getChildContext).to.have.been.calledOnce; + + // initial render does not invoke anything but render(): + expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); + + CONTEXT.foo = 'bar'; + render(<Outer {...PROPS} />, scratch, scratch.lastChild); + + expect(Outer.prototype.getChildContext).to.have.been.calledTwice; + + expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); + expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); + expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); + expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); + expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); + + + /* Future: + * Newly created context objects are *not* currently cloned. + * This test checks that they *are* cloned. + */ + // Inner.prototype.render.reset(); + // CONTEXT.foo = 'baz'; + // inner.forceUpdate(); + // expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, { a:'a', foo:'bar' }); + }); + + it('should pass context to direct children', () => { + const CONTEXT = { a:'a' }; + const PROPS = { b:'b' }; + + class Outer extends Component { + getChildContext() { + return CONTEXT; + } + render(props) { + return <Inner {...props} />; + } + } + sinon.spy(Outer.prototype, 'getChildContext'); + + class Inner extends Component { + shouldComponentUpdate() { return true; } + componentWillReceiveProps() {} + componentWillUpdate() {} + componentDidUpdate() {} + render(props, state, context) { + return <div>{ context && context.a }</div>; + } + } + sinon.spy(Inner.prototype, 'shouldComponentUpdate'); + sinon.spy(Inner.prototype, 'componentWillReceiveProps'); + sinon.spy(Inner.prototype, 'componentWillUpdate'); + sinon.spy(Inner.prototype, 'componentDidUpdate'); + sinon.spy(Inner.prototype, 'render'); + + render(<Outer />, scratch, scratch.lastChild); + + expect(Outer.prototype.getChildContext).to.have.been.calledOnce; + + // initial render does not invoke anything but render(): + expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); + + CONTEXT.foo = 'bar'; + render(<Outer {...PROPS} />, scratch, scratch.lastChild); + + expect(Outer.prototype.getChildContext).to.have.been.calledTwice; + + expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); + expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); + expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); + expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); + expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); + + // make sure render() could make use of context.a + expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] })); + }); + + it('should preserve existing context properties when creating child contexts', () => { + let outerContext = { outer:true }, + innerContext = { inner:true }; + class Outer extends Component { + getChildContext() { + return { outerContext }; + } + render() { + return <div><Inner /></div>; + } + } + + class Inner extends Component { + getChildContext() { + return { innerContext }; + } + render() { + return <InnerMost />; + } + } + + class InnerMost extends Component { + render() { + return <strong>test</strong>; + } + } + + sinon.spy(Inner.prototype, 'render'); + sinon.spy(InnerMost.prototype, 'render'); + + render(<Outer />, scratch); + + expect(Inner.prototype.render).to.have.been.calledWith({}, {}, { outerContext }); + expect(InnerMost.prototype.render).to.have.been.calledWith({}, {}, { outerContext, innerContext }); + }); +}); diff --git a/thirdparty/preact/test/browser/keys.js b/thirdparty/preact/test/browser/keys.js new file mode 100644 index 000000000..e0a6b9ae8 --- /dev/null +++ b/thirdparty/preact/test/browser/keys.js @@ -0,0 +1,85 @@ +import { h, Component, render } from '../../src/preact'; +/** @jsx h */ + +describe('keys', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + // See developit/preact-compat#21 + it('should remove orphaned keyed nodes', () => { + let root = render(( + <div> + <div>1</div> + <li key="a">a</li> + </div> + ), scratch); + + root = render(( + <div> + <div>2</div> + <li key="b">b</li> + </div> + ), scratch, root); + + expect(scratch.innerHTML).to.equal('<div><div>2</div><li>b</li></div>'); + }); + + it('should set VNode#key property', () => { + expect(<div />).to.have.property('key').that.is.empty; + expect(<div a="a" />).to.have.property('key').that.is.empty; + expect(<div key="1" />).to.have.property('key', '1'); + }); + + it('should remove keyed nodes (#232)', () => { + class App extends Component { + componentDidMount() { + setTimeout(() => this.setState({opened: true,loading: true}), 10); + setTimeout(() => this.setState({opened: true,loading: false}), 20); + } + + render({ opened, loading }) { + return ( + <BusyIndicator id="app" busy={loading}> + <div>This div needs to be here for this to break</div> + { opened && !loading && <div>{[]}</div> } + </BusyIndicator> + ); + } + } + + class BusyIndicator extends Component { + render({ children, busy }) { + return <div class={busy ? "busy" : ""}> + { children && children.length ? children : <div class="busy-placeholder"></div> } + <div class="indicator"> + <div>indicator</div> + <div>indicator</div> + <div>indicator</div> + </div> + </div>; + } + } + + let root; + + root = render(<App />, scratch, root); + root = render(<App opened loading />, scratch, root); + root = render(<App opened />, scratch, root); + + let html = String(root.innerHTML).replace(/ class=""/g, ''); + expect(html).to.equal('<div>This div needs to be here for this to break</div><div></div><div class="indicator"><div>indicator</div><div>indicator</div><div>indicator</div></div>'); + }); +}); diff --git a/thirdparty/preact/test/browser/lifecycle.js b/thirdparty/preact/test/browser/lifecycle.js new file mode 100644 index 000000000..d6204ca8f --- /dev/null +++ b/thirdparty/preact/test/browser/lifecycle.js @@ -0,0 +1,493 @@ +import { h, render, rerender, Component } from '../../src/preact'; +/** @jsx h */ + +let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) ); + +describe('Lifecycle methods', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + + describe('#componentWillUpdate', () => { + it('should NOT be called on initial render', () => { + class ReceivePropsComponent extends Component { + componentWillUpdate() {} + render() { + return <div />; + } + } + sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); + render(<ReceivePropsComponent />, scratch); + expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called; + }); + + it('should be called when rerender with new props from parent', () => { + let doRender; + class Outer extends Component { + constructor(p, c) { + super(p, c); + this.state = { i: 0 }; + } + componentDidMount() { + doRender = () => this.setState({ i: this.state.i + 1 }); + } + render(props, { i }) { + return <Inner i={i} {...props} />; + } + } + class Inner extends Component { + componentWillUpdate(nextProps, nextState) { + expect(nextProps).to.be.deep.equal({i: 1}); + expect(nextState).to.be.deep.equal({}); + } + render() { + return <div />; + } + } + sinon.spy(Inner.prototype, 'componentWillUpdate'); + sinon.spy(Outer.prototype, 'componentDidMount'); + + // Initial render + render(<Outer />, scratch); + expect(Inner.prototype.componentWillUpdate).not.to.have.been.called; + + // Rerender inner with new props + doRender(); + rerender(); + expect(Inner.prototype.componentWillUpdate).to.have.been.called; + }); + + it('should be called on new state', () => { + let doRender; + class ReceivePropsComponent extends Component { + componentWillUpdate() {} + componentDidMount() { + doRender = () => this.setState({ i: this.state.i + 1 }); + } + render() { + return <div />; + } + } + sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); + render(<ReceivePropsComponent />, scratch); + expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called; + + doRender(); + rerender(); + expect(ReceivePropsComponent.prototype.componentWillUpdate).to.have.been.called; + }); + }); + + describe('#componentWillReceiveProps', () => { + it('should NOT be called on initial render', () => { + class ReceivePropsComponent extends Component { + componentWillReceiveProps() {} + render() { + return <div />; + } + } + sinon.spy(ReceivePropsComponent.prototype, 'componentWillReceiveProps'); + render(<ReceivePropsComponent />, scratch); + expect(ReceivePropsComponent.prototype.componentWillReceiveProps).not.to.have.been.called; + }); + + it('should be called when rerender with new props from parent', () => { + let doRender; + class Outer extends Component { + constructor(p, c) { + super(p, c); + this.state = { i: 0 }; + } + componentDidMount() { + doRender = () => this.setState({ i: this.state.i + 1 }); + } + render(props, { i }) { + return <Inner i={i} {...props} />; + } + } + class Inner extends Component { + componentWillMount() { + expect(this.props.i).to.be.equal(0); + } + componentWillReceiveProps(nextProps) { + expect(nextProps.i).to.be.equal(1); + } + render() { + return <div />; + } + } + sinon.spy(Inner.prototype, 'componentWillReceiveProps'); + sinon.spy(Outer.prototype, 'componentDidMount'); + + // Initial render + render(<Outer />, scratch); + expect(Inner.prototype.componentWillReceiveProps).not.to.have.been.called; + + // Rerender inner with new props + doRender(); + rerender(); + expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; + }); + + it('should be called in right execution order', () => { + let doRender; + class Outer extends Component { + constructor(p, c) { + super(p, c); + this.state = { i: 0 }; + } + componentDidMount() { + doRender = () => this.setState({ i: this.state.i + 1 }); + } + render(props, { i }) { + return <Inner i={i} {...props} />; + } + } + class Inner extends Component { + componentDidUpdate() { + expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; + expect(Inner.prototype.componentWillUpdate).to.have.been.called; + } + componentWillReceiveProps() { + expect(Inner.prototype.componentWillUpdate).not.to.have.been.called; + expect(Inner.prototype.componentDidUpdate).not.to.have.been.called; + } + componentWillUpdate() { + expect(Inner.prototype.componentWillReceiveProps).to.have.been.called; + expect(Inner.prototype.componentDidUpdate).not.to.have.been.called; + } + render() { + return <div />; + } + } + sinon.spy(Inner.prototype, 'componentWillReceiveProps'); + sinon.spy(Inner.prototype, 'componentDidUpdate'); + sinon.spy(Inner.prototype, 'componentWillUpdate'); + sinon.spy(Outer.prototype, 'componentDidMount'); + + render(<Outer />, scratch); + doRender(); + rerender(); + + expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledBefore(Inner.prototype.componentWillUpdate); + expect(Inner.prototype.componentWillUpdate).to.have.been.calledBefore(Inner.prototype.componentDidUpdate); + }); + }); + + + let _it = it; + describe('#constructor and component(Did|Will)(Mount|Unmount)', () => { + /* global DISABLE_FLAKEY */ + let it = DISABLE_FLAKEY ? xit : _it; + + let setState; + class Outer extends Component { + constructor(p, c) { + super(p, c); + this.state = { show:true }; + setState = s => this.setState(s); + } + render(props, { show }) { + return ( + <div> + { show && ( + <Inner {...props} /> + ) } + </div> + ); + } + } + + class LifecycleTestComponent extends Component { + constructor(p, c) { super(p, c); this._constructor(); } + _constructor() {} + componentWillMount() {} + componentDidMount() {} + componentWillUnmount() {} + componentDidUnmount() {} + render() { return <div />; } + } + + class Inner extends LifecycleTestComponent { + render() { + return ( + <div> + <InnerMost /> + </div> + ); + } + } + + class InnerMost extends LifecycleTestComponent { + render() { return <div />; } + } + + let spies = ['_constructor', 'componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; + + let verifyLifycycleMethods = (TestComponent) => { + let proto = TestComponent.prototype; + spies.forEach( s => sinon.spy(proto, s) ); + let reset = () => spies.forEach( s => proto[s].reset() ); + + it('should be invoked for components on initial render', () => { + reset(); + render(<Outer />, scratch); + expect(proto._constructor).to.have.been.called; + expect(proto.componentDidMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + }); + + it('should be invoked for components on unmount', () => { + reset(); + setState({ show:false }); + rerender(); + + expect(proto.componentDidUnmount).to.have.been.called; + expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); + expect(proto.componentDidUnmount).to.have.been.called; + }); + + it('should be invoked for components on re-render', () => { + reset(); + setState({ show:true }); + rerender(); + + expect(proto._constructor).to.have.been.called; + expect(proto.componentDidMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + }); + }; + + describe('inner components', () => { + verifyLifycycleMethods(Inner); + }); + + describe('innermost components', () => { + verifyLifycycleMethods(InnerMost); + }); + + describe('when shouldComponentUpdate() returns false', () => { + let setState; + + class Outer extends Component { + constructor() { + super(); + this.state = { show:true }; + setState = s => this.setState(s); + } + render(props, { show }) { + return ( + <div> + { show && ( + <div> + <Inner {...props} /> + </div> + ) } + </div> + ); + } + } + + class Inner extends Component { + shouldComponentUpdate(){ return false; } + componentWillMount() {} + componentDidMount() {} + componentWillUnmount() {} + componentDidUnmount() {} + render() { + return <div />; + } + } + + let proto = Inner.prototype; + let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; + spies.forEach( s => sinon.spy(proto, s) ); + + let reset = () => spies.forEach( s => proto[s].reset() ); + + beforeEach( () => reset() ); + + it('should be invoke normally on initial mount', () => { + render(<Outer />, scratch); + expect(proto.componentWillMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + }); + + it('should be invoked normally on unmount', () => { + setState({ show:false }); + rerender(); + + expect(proto.componentWillUnmount).to.have.been.called; + expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); + expect(proto.componentDidUnmount).to.have.been.called; + }); + + it('should still invoke mount for shouldComponentUpdate():false', () => { + setState({ show:true }); + rerender(); + + expect(proto.componentWillMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + }); + + it('should still invoke unmount for shouldComponentUpdate():false', () => { + setState({ show:false }); + rerender(); + + expect(proto.componentWillUnmount).to.have.been.called; + expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); + expect(proto.componentDidUnmount).to.have.been.called; + }); + }); + }); + + describe('Lifecycle DOM Timing', () => { + it('should be invoked when dom does (DidMount, WillUnmount) or does not (WillMount, DidUnmount) exist', () => { + let setState; + class Outer extends Component { + constructor() { + super(); + this.state = { show:true }; + setState = s => { + this.setState(s); + this.forceUpdate(); + }; + } + componentWillMount() { + expect(document.getElementById('OuterDiv'), 'Outer componentWillMount').to.not.exist; + } + componentDidMount() { + expect(document.getElementById('OuterDiv'), 'Outer componentDidMount').to.exist; + } + componentWillUnmount() { + expect(document.getElementById('OuterDiv'), 'Outer componentWillUnmount').to.exist; + } + componentDidUnmount() { + expect(document.getElementById('OuterDiv'), 'Outer componentDidUnmount').to.not.exist; + } + render(props, { show }) { + return ( + <div id="OuterDiv"> + { show && ( + <div> + <Inner {...props} /> + </div> + ) } + </div> + ); + } + } + + class Inner extends Component { + componentWillMount() { + expect(document.getElementById('InnerDiv'), 'Inner componentWillMount').to.not.exist; + } + componentDidMount() { + expect(document.getElementById('InnerDiv'), 'Inner componentDidMount').to.exist; + } + componentWillUnmount() { + // @TODO Component mounted into elements (non-components) + // are currently unmounted after those elements, so their + // DOM is unmounted prior to the method being called. + //expect(document.getElementById('InnerDiv'), 'Inner componentWillUnmount').to.exist; + } + componentDidUnmount() { + expect(document.getElementById('InnerDiv'), 'Inner componentDidUnmount').to.not.exist; + } + + render() { + return <div id="InnerDiv" />; + } + } + + let proto = Inner.prototype; + let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount']; + spies.forEach( s => sinon.spy(proto, s) ); + + let reset = () => spies.forEach( s => proto[s].reset() ); + + render(<Outer />, scratch); + expect(proto.componentWillMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + + reset(); + setState({ show:false }); + + expect(proto.componentWillUnmount).to.have.been.called; + expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount); + expect(proto.componentDidUnmount).to.have.been.called; + + reset(); + setState({ show:true }); + + expect(proto.componentWillMount).to.have.been.called; + expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount); + expect(proto.componentDidMount).to.have.been.called; + }); + + it('should remove this.base for HOC', () => { + let createComponent = (name, fn) => { + class C extends Component { + componentWillUnmount() { + expect(this.base, `${name}.componentWillUnmount`).to.exist; + } + componentDidUnmount() { + expect(this.base, `${name}.componentDidUnmount`).not.to.exist; + } + render(props) { return fn(props); } + } + spyAll(C.prototype); + return C; + }; + + class Wrapper extends Component { + render({ children }) { + return <div class="wrapper">{children}</div>; + } + } + + let One = createComponent('One', () => <Wrapper>one</Wrapper> ); + let Two = createComponent('Two', () => <Wrapper>two</Wrapper> ); + let Three = createComponent('Three', () => <Wrapper>three</Wrapper> ); + + let components = [One, Two, Three]; + + let Selector = createComponent('Selector', ({ page }) => { + let Child = components[page]; + return <Child />; + }); + + class App extends Component { + render(_, { page }) { + return <Selector page={page} />; + } + } + + let app; + render(<App ref={ c => app=c } />, scratch); + + for (let i=0; i<20; i++) { + app.setState({ page: i%components.length }); + app.forceUpdate(); + } + }); + }); +}); diff --git a/thirdparty/preact/test/browser/linked-state.js b/thirdparty/preact/test/browser/linked-state.js new file mode 100644 index 000000000..1ca84cdc6 --- /dev/null +++ b/thirdparty/preact/test/browser/linked-state.js @@ -0,0 +1,98 @@ +import { Component } from '../../src/preact'; +import { createLinkedState } from '../../src/linked-state'; + +describe('linked-state', () => { + class TestComponent extends Component { } + let testComponent, linkFunction; + + before( () => { + testComponent = new TestComponent(); + sinon.spy(TestComponent.prototype, 'setState'); + }); + + describe('createLinkedState without eventPath argument', () => { + + before( () => { + linkFunction = createLinkedState(testComponent,'testStateKey'); + expect(linkFunction).to.be.a('function'); + }); + + beforeEach( () => { + TestComponent.prototype['setState'].reset(); + }); + + it('should use value attribute on text input when no eventPath is supplied', () => { + let element = document.createElement('input'); + element.type= 'text'; + element.value = 'newValue'; + + linkFunction({ currentTarget: element }); + + expect(TestComponent.prototype.setState).to.have.been.calledOnce; + expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'newValue'}); + + linkFunction.call(element); + + expect(TestComponent.prototype.setState).to.have.been.calledTwice; + expect(TestComponent.prototype.setState.secondCall).to.have.been.calledWith({'testStateKey': 'newValue'}); + }); + + it('should use checked attribute on checkbox input when no eventPath is supplied', () => { + let checkboxElement = document.createElement('input'); + checkboxElement.type= 'checkbox'; + checkboxElement.checked = true; + + linkFunction({ currentTarget: checkboxElement }); + + expect(TestComponent.prototype.setState).to.have.been.calledOnce; + expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); + }); + + it('should use checked attribute on radio input when no eventPath is supplied', () => { + let radioElement = document.createElement('input'); + radioElement.type= 'radio'; + radioElement.checked = true; + + linkFunction({ currentTarget: radioElement }); + + expect(TestComponent.prototype.setState).to.have.been.calledOnce; + expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); + }); + + + it('should set dot notated state key appropriately', () => { + linkFunction = createLinkedState(testComponent,'nested.state.key'); + let element = document.createElement('input'); + element.type= 'text'; + element.value = 'newValue'; + + linkFunction({ currentTarget: element }); + + expect(TestComponent.prototype.setState).to.have.been.calledOnce; + expect(TestComponent.prototype.setState).to.have.been.calledWith({nested: {state: {key: 'newValue'}}}); + }); + + }); + + describe('createLinkedState with eventPath argument', () => { + + before( () => { + linkFunction = createLinkedState(testComponent,'testStateKey', 'nested.path'); + expect(linkFunction).to.be.a('function'); + }); + + beforeEach( () => { + TestComponent.prototype['setState'].reset(); + }); + + it('should give precedence to nested.path on event over nested.path on component', () => { + let event = {nested: {path: 'nestedPathValueFromEvent'}}; + let component = {_component: {nested: {path: 'nestedPathValueFromComponent'}}}; + + linkFunction.call(component, event); + + expect(TestComponent.prototype.setState).to.have.been.calledOnce; + expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'nestedPathValueFromEvent'}); + }); + }); +}); diff --git a/thirdparty/preact/test/browser/performance.js b/thirdparty/preact/test/browser/performance.js new file mode 100644 index 000000000..e1f7d7956 --- /dev/null +++ b/thirdparty/preact/test/browser/performance.js @@ -0,0 +1,245 @@ +/*global coverage, ENABLE_PERFORMANCE, NODE_ENV*/ +/*eslint no-console:0*/ +/** @jsx h */ + +let { h, Component, render } = require(NODE_ENV==='production' ? '../../dist/preact.min.js' : '../../src/preact'); + +const MULTIPLIER = ENABLE_PERFORMANCE ? (coverage ? 5 : 1) : 999999; + + +let now = typeof performance!=='undefined' && performance.now ? () => performance.now() : () => +new Date(); + +function loop(iter, time) { + let start = now(), + count = 0; + while ( now()-start < time ) { + count++; + iter(); + } + return count; +} + + +function benchmark(iter, callback) { + let a = 0; + function noop() { + try { a++; } finally { a += Math.random(); } + } + + // warm + for (let i=3; i--; ) noop(), iter(); + + let count = 5, + time = 200, + passes = 0, + noops = loop(noop, time), + iterations = 0; + + function next() { + iterations += loop(iter, time); + setTimeout(++passes===count ? done : next, 10); + } + + function done() { + let ticks = Math.round(noops / iterations * count), + hz = iterations / count / time * 1000, + message = `${hz|0}/s (${ticks} ticks)`; + callback({ iterations, noops, count, time, ticks, hz, message }); + } + + next(); +} + + +describe('performance', function() { + let scratch; + + this.timeout(10000); + + before( () => { + if (coverage) { + console.warn('WARNING: Code coverage is enabled, which dramatically reduces performance. Do not pay attention to these numbers.'); + } + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + it('should rerender without changes fast', done => { + let jsx = ( + <div class="foo bar" data-foo="bar" p={2}> + <header> + <h1 class="asdf">a {'b'} c {0} d</h1> + <nav> + <a href="/foo">Foo</a> + <a href="/bar">Bar</a> + </nav> + </header> + <main> + <form onSubmit={()=>{}}> + <input type="checkbox" checked={true} /> + <input type="checkbox" checked={false} /> + <fieldset> + <label><input type="radio" checked /></label> + <label><input type="radio" /></label> + </fieldset> + <button-bar> + <button style="width:10px; height:10px; border:1px solid #FFF;">Normal CSS</button> + <button style="top:0 ; right: 20">Poor CSS</button> + <button style="invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;" icon>Poorer CSS</button> + <button style={{ margin:0, padding:'10px', overflow:'visible' }}>Object CSS</button> + </button-bar> + </form> + </main> + </div> + ); + + let root; + benchmark( () => { + root = render(jsx, scratch, root); + }, ({ ticks, message }) => { + console.log(`PERF: empty diff: ${message}`); + expect(ticks).to.be.below(350 * MULTIPLIER); + done(); + }); + }); + + it('should rerender repeated trees fast', done => { + class Header extends Component { + render() { + return ( + <header> + <h1 class="asdf">a {'b'} c {0} d</h1> + <nav> + <a href="/foo">Foo</a> + <a href="/bar">Bar</a> + </nav> + </header> + ); + } + } + class Form extends Component { + render() { + return ( + <form onSubmit={()=>{}}> + <input type="checkbox" checked={true} /> + <input type="checkbox" checked={false} /> + <fieldset> + <label><input type="radio" checked /></label> + <label><input type="radio" /></label> + </fieldset> + <ButtonBar /> + </form> + ); + } + } + class ButtonBar extends Component { + render() { + return ( + <button-bar> + <Button style="width:10px; height:10px; border:1px solid #FFF;">Normal CSS</Button> + <Button style="top:0 ; right: 20">Poor CSS</Button> + <Button style="invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;" icon>Poorer CSS</Button> + <Button style={{ margin:0, padding:'10px', overflow:'visible' }}>Object CSS</Button> + </button-bar> + ); + } + } + class Button extends Component { + render(props) { + return <button {...props} />; + } + } + class Main extends Component { + render() { + return <Form />; + } + } + class Root extends Component { + render() { + return ( + <div class="foo bar" data-foo="bar" p={2}> + <Header /> + <Main /> + </div> + ); + } + } + class Empty extends Component { + render() { + return <div />; + } + } + class Parent extends Component { + render({ child:C }) { + return <C />; + } + } + + let root; + benchmark( () => { + root = render(<Parent child={Root} />, scratch, root); + root = render(<Parent child={Empty} />, scratch, root); + }, ({ ticks, message }) => { + console.log(`PERF: repeat diff: ${message}`); + expect(ticks).to.be.below(2000 * MULTIPLIER); + done(); + }); + }); + + it('should construct large VDOM trees fast', done => { + const FIELDS = []; + for (let i=100; i--; ) FIELDS.push((i*999).toString(36)); + + let out = []; + function digest(vnode) { + out.push(vnode); + out.length = 0; + } + benchmark( () => { + digest( + <div class="foo bar" data-foo="bar" p={2}> + <header> + <h1 class="asdf">a {'b'} c {0} d</h1> + <nav> + <a href="/foo">Foo</a> + <a href="/bar">Bar</a> + </nav> + </header> + <main> + <form onSubmit={()=>{}}> + <input type="checkbox" checked /> + <input type="checkbox" /> + <fieldset> + { FIELDS.map( field => ( + <label> + {field}: + <input placeholder={field} /> + </label> + )) } + </fieldset> + <button-bar> + <button style="width:10px; height:10px; border:1px solid #FFF;">Normal CSS</button> + <button style="top:0 ; right: 20">Poor CSS</button> + <button style="invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;" icon>Poorer CSS</button> + <button style={{ margin:0, padding:'10px', overflow:'visible' }}>Object CSS</button> + </button-bar> + </form> + </main> + </div> + ); + }, ({ ticks, message }) => { + console.log(`PERF: large VTree: ${message}`); + expect(ticks).to.be.below(2000 * MULTIPLIER); + done(); + }); + }); +}); diff --git a/thirdparty/preact/test/browser/refs.js b/thirdparty/preact/test/browser/refs.js new file mode 100644 index 000000000..89678b76e --- /dev/null +++ b/thirdparty/preact/test/browser/refs.js @@ -0,0 +1,287 @@ +import { h, render, Component } from '../../src/preact'; +/** @jsx h */ + +// gives call count and argument errors names (otherwise sinon just uses "spy"): +let spy = (name, ...args) => { + let spy = sinon.spy(...args); + spy.displayName = `spy('${name}')`; + return spy; +}; + +describe('refs', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + it('should invoke refs in render()', () => { + let ref = spy('ref'); + render(<div ref={ref} />, scratch); + expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); + }); + + it('should invoke refs in Component.render()', () => { + let outer = spy('outer'), + inner = spy('inner'); + class Foo extends Component { + render() { + return ( + <div ref={outer}> + <span ref={inner} /> + </div> + ); + } + } + render(<Foo />, scratch); + + expect(outer).to.have.been.calledWith(scratch.firstChild); + expect(inner).to.have.been.calledWith(scratch.firstChild.firstChild); + }); + + it('should pass components to ref functions', () => { + let ref = spy('ref'), + instance; + class Foo extends Component { + constructor() { + super(); + instance = this; + } + render() { + return <div />; + } + } + render(<Foo ref={ref} />, scratch); + + expect(ref).to.have.been.calledOnce.and.calledWith(instance); + }); + + it('should pass rendered DOM from functional components to ref functions', () => { + let ref = spy('ref'); + + const Foo = () => <div />; + + let root = render(<Foo ref={ref} />, scratch); + expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); + + ref.reset(); + render(<Foo ref={ref} />, scratch, root); + expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild); + + ref.reset(); + render(<span />, scratch, root); + expect(ref).to.have.been.calledOnce.and.calledWith(null); + }); + + it('should pass children to ref functions', () => { + let outer = spy('outer'), + inner = spy('inner'), + rerender, inst; + class Outer extends Component { + constructor() { + super(); + rerender = () => this.forceUpdate(); + } + render() { + return ( + <div> + <Inner ref={outer} /> + </div> + ); + } + } + class Inner extends Component { + constructor() { + super(); + inst = this; + } + render() { + return <span ref={inner} />; + } + } + + let root = render(<Outer />, scratch); + + expect(outer).to.have.been.calledOnce.and.calledWith(inst); + expect(inner).to.have.been.calledOnce.and.calledWith(inst.base); + + outer.reset(); + inner.reset(); + + rerender(); + + expect(outer).to.have.been.calledOnce.and.calledWith(inst); + expect(inner).to.have.been.calledOnce.and.calledWith(inst.base); + + outer.reset(); + inner.reset(); + + render(<div />, scratch, root); + + expect(outer).to.have.been.calledOnce.and.calledWith(null); + expect(inner).to.have.been.calledOnce.and.calledWith(null); + }); + + it('should pass high-order children to ref functions', () => { + let outer = spy('outer'), + inner = spy('inner'), + innermost = spy('innermost'), + outerInst, + innerInst; + class Outer extends Component { + constructor() { + super(); + outerInst = this; + } + render() { + return <Inner ref={inner} />; + } + } + class Inner extends Component { + constructor() { + super(); + innerInst = this; + } + render() { + return <span ref={innermost} />; + } + } + + let root = render(<Outer ref={outer} />, scratch); + + expect(outer, 'outer initial').to.have.been.calledOnce.and.calledWith(outerInst); + expect(inner, 'inner initial').to.have.been.calledOnce.and.calledWith(innerInst); + expect(innermost, 'innerMost initial').to.have.been.calledOnce.and.calledWith(innerInst.base); + + outer.reset(); + inner.reset(); + innermost.reset(); + root = render(<Outer ref={outer} />, scratch, root); + + expect(outer, 'outer update').to.have.been.calledOnce.and.calledWith(outerInst); + expect(inner, 'inner update').to.have.been.calledOnce.and.calledWith(innerInst); + expect(innermost, 'innerMost update').to.have.been.calledOnce.and.calledWith(innerInst.base); + + outer.reset(); + inner.reset(); + innermost.reset(); + root = render(<div />, scratch, root); + + expect(outer, 'outer unmount').to.have.been.calledOnce.and.calledWith(null); + expect(inner, 'inner unmount').to.have.been.calledOnce.and.calledWith(null); + expect(innermost, 'innerMost unmount').to.have.been.calledOnce.and.calledWith(null); + }); + + it('should not pass ref into component as a prop', () => { + let foo = spy('foo'), + bar = spy('bar'); + + class Foo extends Component { + render(){ return <div />; } + } + const Bar = spy('Bar', () => <div />); + + sinon.spy(Foo.prototype, 'render'); + + render(( + <div> + <Foo ref={foo} a="a" /> + <Bar ref={bar} b="b" /> + </div> + ), scratch); + + expect(Foo.prototype.render).to.have.been.calledWithExactly({ a:'a' }, { }, { }); + expect(Bar).to.have.been.calledWithExactly({ b:'b', ref:bar }, { }); + }); + + // Test for #232 + it('should only null refs after unmount', () => { + let root, outer, inner; + + class TestUnmount extends Component { + componentWillUnmount() { + expect(this).to.have.property('outer', outer); + expect(this).to.have.property('inner', inner); + } + + componentDidUnmount() { + expect(this).to.have.property('outer', null); + expect(this).to.have.property('inner', null); + } + + render() { + return ( + <div id="outer" ref={ c => this.outer=c }> + <div id="inner" ref={ c => this.inner=c } /> + </div> + ); + } + } + + sinon.spy(TestUnmount.prototype, 'componentWillUnmount'); + sinon.spy(TestUnmount.prototype, 'componentDidUnmount'); + + root = render(<div><TestUnmount /></div>, scratch, root); + outer = scratch.querySelector('#outer'); + inner = scratch.querySelector('#inner'); + + expect(TestUnmount.prototype.componentWillUnmount).not.to.have.been.called; + expect(TestUnmount.prototype.componentDidUnmount).not.to.have.been.called; + + root = render(<div />, scratch, root); + + expect(TestUnmount.prototype.componentWillUnmount).to.have.been.calledOnce; + expect(TestUnmount.prototype.componentDidUnmount).to.have.been.calledOnce; + }); + + it('should null and re-invoke refs when swapping component root element type', () => { + let inst; + + class App extends Component { + render() { + return <div><Child /></div>; + } + } + + class Child extends Component { + constructor(props, context) { + super(props, context); + this.state = { show:false }; + inst = this; + } + handleMount(){} + render(_, { show }) { + if (!show) return <div id="div" ref={this.handleMount}></div>; + return <span id="span" ref={this.handleMount}>some test content</span>; + } + } + sinon.spy(Child.prototype, 'handleMount'); + + render(<App />, scratch); + expect(inst.handleMount).to.have.been.calledOnce.and.calledWith(scratch.querySelector('#div')); + inst.handleMount.reset(); + + inst.setState({ show:true }); + inst.forceUpdate(); + expect(inst.handleMount).to.have.been.calledTwice; + expect(inst.handleMount.firstCall).to.have.been.calledWith(null); + expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#span')); + inst.handleMount.reset(); + + inst.setState({ show:false }); + inst.forceUpdate(); + expect(inst.handleMount).to.have.been.calledTwice; + expect(inst.handleMount.firstCall).to.have.been.calledWith(null); + expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#div')); + }); +}); diff --git a/thirdparty/preact/test/browser/render.js b/thirdparty/preact/test/browser/render.js new file mode 100644 index 000000000..5d18fb282 --- /dev/null +++ b/thirdparty/preact/test/browser/render.js @@ -0,0 +1,439 @@ +/* global DISABLE_FLAKEY */ + +import { h, render } from '../../src/preact'; +/** @jsx h */ + +function getAttributes(node) { + let attrs = {}; + 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; + }); +} + +describe('render()', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + it('should create empty nodes (<* />)', () => { + render(<div />, scratch); + expect(scratch.childNodes) + .to.have.length(1) + .and.to.have.deep.property('0.nodeName', 'DIV'); + + scratch.innerHTML = ''; + + render(<span />, scratch); + expect(scratch.childNodes) + .to.have.length(1) + .and.to.have.deep.property('0.nodeName', 'SPAN'); + + scratch.innerHTML = ''; + + render(<foo />, scratch); + render(<x-bar />, scratch); + expect(scratch.childNodes).to.have.length(2); + expect(scratch.childNodes[0]).to.have.property('nodeName', 'FOO'); + expect(scratch.childNodes[1]).to.have.property('nodeName', 'X-BAR'); + }); + + it('should nest empty nodes', () => { + render(( + <div> + <span /> + <foo /> + <x-bar /> + </div> + ), scratch); + + expect(scratch.childNodes) + .to.have.length(1) + .and.to.have.deep.property('0.nodeName', 'DIV'); + + let c = scratch.childNodes[0].childNodes; + expect(c).to.have.length(3); + expect(c).to.have.deep.property('0.nodeName', 'SPAN'); + expect(c).to.have.deep.property('1.nodeName', 'FOO'); + expect(c).to.have.deep.property('2.nodeName', 'X-BAR'); + }); + + it('should not render falsey values', () => { + render(( + <div> + {null},{undefined},{false},{0},{NaN} + </div> + ), scratch); + + expect(scratch.firstChild).to.have.property('innerHTML', ',,,0,NaN'); + }); + + it('should clear falsey attributes', () => { + let root = render(( + <div anull="anull" aundefined="aundefined" afalse="afalse" anan="aNaN" a0="a0" /> + ), scratch); + + root = render(( + <div anull={null} aundefined={undefined} afalse={false} anan={NaN} a0={0} /> + ), scratch, root); + + expect(getAttributes(scratch.firstChild), 'from previous truthy values').to.eql({ + a0: '0', + anan: 'NaN' + }); + + scratch.innerHTML = ''; + + root = render(( + <div anull={null} aundefined={undefined} afalse={false} anan={NaN} a0={0} /> + ), scratch); + + expect(getAttributes(scratch.firstChild), 'initial render').to.eql({ + a0: '0', + anan: 'NaN' + }); + }); + + it('should clear falsey input values', () => { + let root = render(( + <div> + <input value={0} /> + <input value={false} /> + <input value={null} /> + <input value={undefined} /> + </div> + ), scratch); + + expect(root.children[0]).to.have.property('value', '0'); + expect(root.children[1]).to.have.property('value', 'false'); + expect(root.children[2]).to.have.property('value', ''); + expect(root.children[3]).to.have.property('value', ''); + }); + + it('should clear falsey DOM properties', () => { + let root; + function test(val) { + root = render(( + <div> + <input value={val} /> + <table border={val} /> + </div> + ), scratch, root); + } + + test('2'); + test(false); + expect(scratch).to.have.property('innerHTML', '<div><input><table></table></div>', 'for false'); + + test('3'); + test(null); + expect(scratch).to.have.property('innerHTML', '<div><input><table></table></div>', 'for null'); + + test('4'); + test(undefined); + expect(scratch).to.have.property('innerHTML', '<div><input><table></table></div>', 'for undefined'); + }); + + it('should apply string attributes', () => { + render(<div foo="bar" data-foo="databar" />, scratch); + + let div = scratch.childNodes[0]; + expect(div).to.have.deep.property('attributes.length', 2); + + expect(div).to.have.deep.property('attributes[0].name', 'foo'); + expect(div).to.have.deep.property('attributes[0].value', 'bar'); + + expect(div).to.have.deep.property('attributes[1].name', 'data-foo'); + expect(div).to.have.deep.property('attributes[1].value', 'databar'); + }); + + it('should apply class as String', () => { + render(<div class="foo" />, scratch); + expect(scratch.childNodes[0]).to.have.property('className', 'foo'); + }); + + it('should alias className to class', () => { + render(<div className="bar" />, scratch); + expect(scratch.childNodes[0]).to.have.property('className', 'bar'); + }); + + it('should apply style as String', () => { + render(<div style="top:5px; position:relative;" />, scratch); + expect(scratch.childNodes[0]).to.have.deep.property('style.cssText') + .that.matches(/top\s*:\s*5px\s*/) + .and.matches(/position\s*:\s*relative\s*/); + }); + + it('should only register on* functions as handlers', () => { + let click = () => {}, + onclick = () => {}; + + let proto = document.createElement('div').constructor.prototype; + + sinon.spy(proto, 'addEventListener'); + + render(<div click={ click } onClick={ onclick } />, scratch); + + expect(scratch.childNodes[0]).to.have.deep.property('attributes.length', 0); + + expect(proto.addEventListener).to.have.been.calledOnce + .and.to.have.been.calledWithExactly('click', sinon.match.func, false); + + proto.addEventListener.restore(); + }); + + it('should add and remove event handlers', () => { + let click = sinon.spy(), + mousedown = sinon.spy(); + + let proto = document.createElement('div').constructor.prototype; + sinon.spy(proto, 'addEventListener'); + sinon.spy(proto, 'removeEventListener'); + + function fireEvent(on, type) { + let e = document.createEvent('Event'); + e.initEvent(type, true, true); + on.dispatchEvent(e); + } + + render(<div onClick={ () => click(1) } onMouseDown={ mousedown } />, scratch); + + expect(proto.addEventListener).to.have.been.calledTwice + .and.to.have.been.calledWith('click') + .and.calledWith('mousedown'); + + fireEvent(scratch.childNodes[0], 'click'); + expect(click).to.have.been.calledOnce + .and.calledWith(1); + + proto.addEventListener.reset(); + click.reset(); + + render(<div onClick={ () => click(2) } />, scratch, scratch.firstChild); + + expect(proto.addEventListener).not.to.have.been.called; + + expect(proto.removeEventListener) + .to.have.been.calledOnce + .and.calledWith('mousedown'); + + fireEvent(scratch.childNodes[0], 'click'); + expect(click).to.have.been.calledOnce + .and.to.have.been.calledWith(2); + + fireEvent(scratch.childNodes[0], 'mousedown'); + expect(mousedown).not.to.have.been.called; + + proto.removeEventListener.reset(); + click.reset(); + mousedown.reset(); + + render(<div />, scratch, scratch.firstChild); + + expect(proto.removeEventListener) + .to.have.been.calledOnce + .and.calledWith('click'); + + fireEvent(scratch.childNodes[0], 'click'); + expect(click).not.to.have.been.called; + + proto.addEventListener.restore(); + proto.removeEventListener.restore(); + }); + + it('should use capturing for events that do not bubble', () => { + let click = sinon.spy(), + focus = sinon.spy(); + + let root = render(( + <div onClick={click} onFocus={focus}> + <button /> + </div> + ), scratch); + + root.firstElementChild.click(); + root.firstElementChild.focus(); + + expect(click, 'click').to.have.been.calledOnce; + + if (DISABLE_FLAKEY!==true) { + // Focus delegation requires a 50b hack I'm not sure we want to incur + expect(focus, 'focus').to.have.been.calledOnce; + + // IE doesn't set it + expect(click).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing + expect(focus).to.have.been.calledWithMatch({ eventPhase: 0 }); // capturing + } + }); + + it('should serialize style objects', () => { + let root = render(( + <div style={{ + color: 'rgb(255, 255, 255)', + background: 'rgb(255, 100, 0)', + backgroundPosition: '10px 10px', + 'background-size': 'cover', + padding: 5, + top: 100, + left: '100%' + }}> + test + </div> + ), scratch); + + let { style } = scratch.childNodes[0]; + expect(style).to.have.property('color').that.equals('rgb(255, 255, 255)'); + expect(style).to.have.property('background').that.contains('rgb(255, 100, 0)'); + expect(style).to.have.property('backgroundPosition').that.equals('10px 10px'); + expect(style).to.have.property('backgroundSize', 'cover'); + expect(style).to.have.property('padding', '5px'); + expect(style).to.have.property('top', '100px'); + expect(style).to.have.property('left', '100%'); + + root = render(( + <div style={{ color: 'rgb(0, 255, 255)' }}>test</div> + ), scratch, root); + + expect(root).to.have.deep.property('style.cssText').that.equals('color: rgb(0, 255, 255);'); + + root = render(( + <div style="display: inline;">test</div> + ), scratch, root); + + expect(root).to.have.deep.property('style.cssText').that.equals('display: inline;'); + + root = render(( + <div style={{ backgroundColor: 'rgb(0, 255, 255)' }}>test</div> + ), scratch, root); + + expect(root).to.have.deep.property('style.cssText').that.equals('background-color: rgb(0, 255, 255);'); + }); + + it('should serialize class/className', () => { + render(<div class={{ + no1: false, + no2: 0, + no3: null, + no4: undefined, + no5: '', + yes1: true, + yes2: 1, + yes3: {}, + yes4: [], + yes5: ' ' + }} />, scratch); + + let { className } = scratch.childNodes[0]; + expect(className).to.be.a.string; + expect(className.split(' ')) + .to.include.members(['yes1', 'yes2', 'yes3', 'yes4', 'yes5']) + .and.not.include.members(['no1', 'no2', 'no3', 'no4', 'no5']); + }); + + it('should support dangerouslySetInnerHTML', () => { + let html = '<b>foo & bar</b>'; + let root = render(<div dangerouslySetInnerHTML={{ __html: html }} />, scratch); + + expect(scratch.firstChild).to.have.property('innerHTML', html); + expect(scratch.innerHTML).to.equal('<div>'+html+'</div>'); + + root = render(<div>a<strong>b</strong></div>, scratch, root); + + expect(scratch).to.have.property('innerHTML', `<div>a<strong>b</strong></div>`); + + root = render(<div dangerouslySetInnerHTML={{ __html: html }} />, scratch, root); + + expect(scratch.innerHTML).to.equal('<div>'+html+'</div>'); + }); + + it('should reconcile mutated DOM attributes', () => { + let check = p => render(<input type="checkbox" checked={p} />, scratch, scratch.lastChild), + value = () => scratch.lastChild.checked, + setValue = p => scratch.lastChild.checked = p; + check(true); + expect(value()).to.equal(true); + check(false); + expect(value()).to.equal(false); + check(true); + expect(value()).to.equal(true); + setValue(true); + check(false); + expect(value()).to.equal(false); + setValue(false); + check(true); + expect(value()).to.equal(true); + }); + + it('should ignore props.children if children are manually specified', () => { + expect( + <div a children={['a', 'b']}>c</div> + ).to.eql( + <div a>c</div> + ); + }); + + it('should reorder child pairs', () => { + let root = render(( + <div> + <a>a</a> + <b>b</b> + </div> + ), scratch, root); + + let a = scratch.firstChild.firstChild; + let b = scratch.firstChild.lastChild; + + expect(a).to.have.property('nodeName', 'A'); + expect(b).to.have.property('nodeName', 'B'); + + root = render(( + <div> + <b>b</b> + <a>a</a> + </div> + ), scratch, root); + + expect(scratch.firstChild.firstChild).to.have.property('nodeName', 'B'); + expect(scratch.firstChild.lastChild).to.have.property('nodeName', 'A'); + expect(scratch.firstChild.firstChild).to.equal(b); + expect(scratch.firstChild.lastChild).to.equal(a); + }); + + // Discussion: https://github.com/developit/preact/issues/287 + ('HTMLDataListElement' in window ? it : xit)('should allow <input list /> to pass through as an attribute', () => { + render(( + <div> + <input type="range" min="0" max="100" list="steplist" /> + <datalist id="steplist"> + <option>0</option> + <option>50</option> + <option>100</option> + </datalist> + </div> + ), scratch); + + let html = scratch.firstElementChild.firstElementChild.outerHTML; + expect(sortAttributes(html)).to.equal(sortAttributes('<input type="range" min="0" max="100" list="steplist">')); + }); +}); diff --git a/thirdparty/preact/test/browser/spec.js b/thirdparty/preact/test/browser/spec.js new file mode 100644 index 000000000..eb48151f0 --- /dev/null +++ b/thirdparty/preact/test/browser/spec.js @@ -0,0 +1,124 @@ +import { h, render, rerender, Component } from '../../src/preact'; +/** @jsx h */ + +describe('Component spec', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + describe('defaultProps', () => { + it('should apply default props on initial render', () => { + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + expect(props).to.be.deep.equal({ + fieldA: 1, fieldB: 2, + fieldC: 1, fieldD: 2 + }); + } + render() { + return <div />; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + render(<WithDefaultProps fieldA={1} fieldB={2} fieldD={2} />, scratch); + }); + + it('should apply default props on rerender', () => { + let doRender; + class Outer extends Component { + constructor() { + super(); + this.state = { i:1 }; + } + componentDidMount() { + doRender = () => this.setState({ i: 2 }); + } + render(props, { i }) { + return <WithDefaultProps fieldA={1} fieldB={i} fieldD={i} />; + } + } + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + this.ctor(props, context); + } + ctor(){} + componentWillReceiveProps() {} + render() { + return <div />; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + + let proto = WithDefaultProps.prototype; + sinon.spy(proto, 'ctor'); + sinon.spy(proto, 'componentWillReceiveProps'); + sinon.spy(proto, 'render'); + + render(<Outer />, scratch); + doRender(); + + const PROPS1 = { + fieldA: 1, fieldB: 1, + fieldC: 1, fieldD: 1 + }; + + const PROPS2 = { + fieldA: 1, fieldB: 2, + fieldC: 1, fieldD: 2 + }; + + expect(proto.ctor).to.have.been.calledWith(PROPS1); + expect(proto.render).to.have.been.calledWith(PROPS1); + + rerender(); + + // expect(proto.ctor).to.have.been.calledWith(PROPS2); + expect(proto.componentWillReceiveProps).to.have.been.calledWith(PROPS2); + expect(proto.render).to.have.been.calledWith(PROPS2); + }); + + // @TODO: migrate this to preact-compat + xit('should cache default props', () => { + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + expect(props).to.be.deep.equal({ + fieldA: 1, fieldB: 2, + fieldC: 1, fieldD: 2, + fieldX: 10 + }); + } + getDefaultProps() { + return { fieldA: 1, fieldB: 1 }; + } + render() { + return <div />; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + sinon.spy(WithDefaultProps.prototype, 'getDefaultProps'); + render(( + <div> + <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} /> + <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} /> + <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} /> + </div> + ), scratch); + expect(WithDefaultProps.prototype.getDefaultProps).to.be.calledOnce; + }); + }); +}); diff --git a/thirdparty/preact/test/browser/svg.js b/thirdparty/preact/test/browser/svg.js new file mode 100644 index 000000000..684f4dd96 --- /dev/null +++ b/thirdparty/preact/test/browser/svg.js @@ -0,0 +1,112 @@ +import { h, render } from '../../src/preact'; +/** @jsx h */ + + +// 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; + }); +} + + +describe('svg', () => { + let scratch; + + before( () => { + scratch = document.createElement('div'); + (document.body || document.documentElement).appendChild(scratch); + }); + + beforeEach( () => { + scratch.innerHTML = ''; + }); + + after( () => { + scratch.parentNode.removeChild(scratch); + scratch = null; + }); + + it('should render SVG to string', () => { + render(( + <svg viewBox="0 0 360 360"> + <path stroke="white" fill="black" d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" /> + </svg> + ), scratch); + + let html = sortAttributes(String(scratch.innerHTML).replace(' xmlns="http://www.w3.org/2000/svg"', '')); + expect(html).to.equal(sortAttributes(` + <svg viewBox="0 0 360 360"> + <path d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" fill="black" stroke="white"></path> + </svg> + `.replace(/[\n\t]+/g,''))); + }); + + it('should render SVG to DOM', () => { + const Demo = () => ( + <svg viewBox="0 0 360 360"> + <path d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z" fill="black" stroke="white" /> + </svg> + ); + render(<Demo />, scratch); + + let html = sortAttributes(String(scratch.innerHTML).replace(' xmlns="http://www.w3.org/2000/svg"', '')); + expect(html).to.equal(sortAttributes('<svg viewBox="0 0 360 360"><path stroke="white" fill="black" d="M 347.1 357.9 L 183.3 256.5 L 13 357.9 V 1.7 h 334.1 v 356.2 Z M 58.5 47.2 v 231.4 l 124.8 -74.1 l 118.3 72.8 V 47.2 H 58.5 Z"></path></svg>')); + }); + + it('should use attributes for className', () => { + const Demo = ({ c }) => ( + <svg viewBox="0 0 360 360" {...(c ? {class:'foo_'+c} : {})}> + <path class={c && ('bar_'+c)} stroke="white" fill="black" d="M347.1 357.9L183.3 256.5 13 357.9V1.7h334.1v356.2zM58.5 47.2v231.4l124.8-74.1 118.3 72.8V47.2H58.5z" /> + </svg> + ); + let root = render(<Demo c="1" />, scratch, root); + sinon.spy(root, 'removeAttribute'); + root = render(<Demo />, scratch, root); + expect(root.removeAttribute).to.have.been.calledOnce.and.calledWith('class'); + root.removeAttribute.restore(); + + root = render(<div />, scratch, root); + root = render(<Demo />, scratch, root); + sinon.spy(root, 'setAttribute'); + root = render(<Demo c="2" />, scratch, root); + expect(root.setAttribute).to.have.been.calledOnce.and.calledWith('class', 'foo_2'); + root.setAttribute.restore(); + root = render(<Demo c="3" />, scratch, root); + root = render(<Demo />, scratch, root); + }); + + it('should still support class attribute', () => { + render(( + <svg viewBox="0 0 1 1" class="foo bar" /> + ), scratch); + + expect(scratch.innerHTML).to.contain(` class="foo bar"`); + }); + + it('should serialize class', () => { + render(( + <svg viewBox="0 0 1 1" class={{ foo: true, bar: false, other: 'hello' }} /> + ), scratch); + + expect(scratch.innerHTML).to.contain(` class="foo other"`); + }); + + it('should switch back to HTML for <foreignObject>', () => { + render(( + <svg> + <g> + <foreignObject> + <a href="#foo">test</a> + </foreignObject> + </g> + </svg> + ), scratch); + + expect(scratch.getElementsByTagName('a')) + .to.have.property('0') + .that.is.a('HTMLAnchorElement'); + }); +}); |