aboutsummaryrefslogtreecommitdiff
path: root/thirdparty/preact/test/browser
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-10-04 11:50:26 +0200
committerFlorian Dold <florian.dold@gmail.com>2016-10-04 11:50:26 +0200
commit133a8c672c26609e9d56d4e184b779ca26971a8c (patch)
treef3aa4779a75279c95553cd68dd2a3bbe5e5b9dde /thirdparty/preact/test/browser
parent0697c987c5314232056a86fa128b518c866d2f12 (diff)
parent30b577138dda685f65a8529be1866afa6e321845 (diff)
downloadwallet-core-133a8c672c26609e9d56d4e184b779ca26971a8c.tar.xz
Merge commit '30b577138dda685f65a8529be1866afa6e321845' as 'thirdparty/preact'
Diffstat (limited to 'thirdparty/preact/test/browser')
-rw-r--r--thirdparty/preact/test/browser/components.js713
-rw-r--r--thirdparty/preact/test/browser/context.js170
-rw-r--r--thirdparty/preact/test/browser/keys.js85
-rw-r--r--thirdparty/preact/test/browser/lifecycle.js493
-rw-r--r--thirdparty/preact/test/browser/linked-state.js98
-rw-r--r--thirdparty/preact/test/browser/performance.js245
-rw-r--r--thirdparty/preact/test/browser/refs.js287
-rw-r--r--thirdparty/preact/test/browser/render.js439
-rw-r--r--thirdparty/preact/test/browser/spec.js124
-rw-r--r--thirdparty/preact/test/browser/svg.js112
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 &amp; 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');
+ });
+});