preact-material-components
Version:
preact wrapper for "Material Components for the web"
547 lines (453 loc) • 12.6 kB
JavaScript
import renderToString from 'preact-render-to-string';
import { rerender } from 'preact';
import React from '../src';
describe('components', () => {
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 be sane', () => {
let props;
class Demo extends React.Component {
render() {
let { a, b } = this.props;
props = this.props;
return <div id="demo">{ this.props.children }</div>;
}
}
let html = renderToString(
<Demo a="b" c="d">inner</Demo>
);
expect(props).to.exist.and.deep.equal({
a: 'b',
c: 'd',
children: 'inner'
});
expect(html).to.equal('<div id="demo">inner</div>');
});
it('should support replaceState()', done => {
class Demo extends React.Component {
render() {
return <div />;
}
}
let render = sinon.spy(Demo.prototype, 'render'),
inst;
React.render(<Demo ref={ c => inst=c } />, scratch);
inst.setState({ foo:'bar', baz:'bat' });
setTimeout( () => {
expect(inst.state).to.eql({ foo:'bar', baz:'bat' });
let callbackState;
let callback = sinon.spy( () => {
callbackState = inst.state;
});
inst.replaceState({}, callback);
setTimeout( () => {
expect(callback).to.have.been.calledOnce;
expect(callbackState).to.eql({});
expect(inst.state).to.eql({});
done();
}, 10);
}, 10);
});
it('should alias props.children', () => {
class Foo extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
let children = ['a', <span>b</span>, <b>c</b>],
foo;
React.render((
<Foo ref={ c => foo=c }>
{ children }
</Foo>
), scratch);
expect(foo).to.exist.and.have.deep.property('props.children').eql(children);
});
it('should single out children before componentWillReceiveProps', () => {
let props;
class Child extends React.Component {
componentWillReceiveProps(newProps) {
props = newProps;
}
}
class Parent extends React.Component {
render() {
return <Child>second</Child>;
}
}
let a = React.render(<Parent/>, scratch);
a.forceUpdate();
expect(props).to.exist.and.deep.equal({
children: 'second'
});
});
it('should support array[object] children', () => {
let children;
class Foo extends React.Component {
render() {
children = this.props.children;
return <div />;
}
}
const data = [{a: ''}];
React.render(<Foo>{ data }</Foo>, scratch);
expect(children).to.exist.and.deep.equal(data);
});
describe('getInitialState', () => {
it('should be invoked for new components', () => {
class Foo extends React.Component {
getInitialState() {
return { foo: 'bar' };
}
render() {
return <div />;
}
}
sinon.spy(Foo.prototype, 'getInitialState');
let a = React.render(<Foo />, scratch);
expect(Foo.prototype.getInitialState).to.have.been.calledOnce;
expect(a.state).to.eql({ foo: 'bar' });
});
});
describe('defaultProps', () => {
it('should support defaultProps for components', () => {
let render = sinon.stub().returns(<div />);
const Foo = React.createClass({
defaultProps: {
foo: 'default foo',
bar: 'default bar'
},
render
});
React.render(<Foo />, scratch);
expect(render).to.have.been.calledWithMatch(Foo.defaultProps);
render.reset();
React.render(<Foo bar="bar" />, scratch);
expect(render).to.have.been.calledWithMatch({ foo:'default foo', bar:'bar' });
});
it('should support defaultProps for pure components', () => {
const Foo = sinon.stub().returns(<div />);
Foo.defaultProps = {
foo: 'default foo',
bar: 'default bar'
};
React.render(<Foo />, scratch);
expect(Foo).to.have.been.calledWithMatch(Foo.defaultProps);
Foo.reset();
React.render(<Foo bar="bar" />, scratch);
expect(Foo).to.have.been.calledWithMatch({ foo:'default foo', bar:'bar' });
});
});
describe('propTypes', () => {
function checkPropTypes(Foo, name = 'Foo') {
sinon.stub(console, 'error');
React.render(<Foo />, scratch);
expect(console.error).to.have.been.calledWithMatch(
'Warning: Failed prop type: The prop `func` is marked as required in `' + name + '`, but its value is `undefined`.'
);
expect(console.error).to.have.been.called;
console.error.reset();
React.render(<Foo func={()=>{}} />, scratch);
expect(console.error).not.to.have.been.called;
React.render(<Foo func={()=>{}} bool="one" />, scratch);
expect(console.error).to.have.been.calledWithMatch(
'Warning: Failed prop type: Invalid prop `bool` of type `string` supplied to `' + name + '`, expected `boolean`.'
);
console.error.restore();
}
it('should support propTypes for ES Class components', () => {
class Foo extends React.Component {
static propTypes = {
func: React.PropTypes.func.isRequired,
bool: React.PropTypes.bool
};
render() {
return <div />;
}
}
checkPropTypes(Foo);
});
it('should support propTypes for createClass components', () => {
const Bar = React.createClass({
propTypes: {
func: React.PropTypes.func.isRequired,
bool: React.PropTypes.bool
},
render: () => <div />
});
checkPropTypes(Bar, 'Bar');
});
it('should support propTypes for pure components', () => {
function Baz() { return <div />; }
Baz.propTypes = {
func: React.PropTypes.func.isRequired,
bool: React.PropTypes.bool
};
checkPropTypes(Baz, 'Baz');
const Bip = () => <div />;
Bip.displayName = 'Bip';
Bip.propTypes = {
func: React.PropTypes.func.isRequired,
bool: React.PropTypes.bool
};
checkPropTypes(Bip, 'Bip');
});
});
describe("mixins", () => {
describe("getDefaultProps", () => {
it('should use a mixin', () => {
const Foo = React.createClass({
mixins: [
{ getDefaultProps: () => ({ a: true }) }
],
render() {
return <div />;
}
});
expect(Foo.defaultProps).to.eql({
a: true
});
});
it('should combine the results', () => {
const Foo = React.createClass({
mixins: [
{ getDefaultProps: () => ({ a: true }) },
{ getDefaultProps: () => ({ b: true }) }
],
getDefaultProps() {
return { c: true };
},
render() {
return <div />;
}
});
expect(Foo.defaultProps).to.eql({
a: true,
b: true,
c: true
});
});
it('should work with statics', () => {
const Foo = React.createClass({
statics: {
a: false
},
getDefaultProps() {
return { b: true, c: this.a };
},
render() {
return <div />;
}
});
expect(Foo.defaultProps).to.eql({
b: true,
c: false
});
});
// Disabled to save bytes
xit('should throw an error for duplicate keys', () => {
expect(() => {
const Foo = React.createClass({
mixins: [
{ getDefaultProps: () => ({ a: true }) }
],
getDefaultProps() {
return { a: true };
},
render() {
return <div />;
}
});
}).to.throw();
});
});
describe("getInitialState", () => {
it('should combine the results', () => {
const Foo = React.createClass({
mixins: [
{ getInitialState: () => ({ a: true }) },
{ getInitialState: () => ({ b: true }) }
],
getInitialState() {
return { c: true };
},
render() {
return <div />;
}
});
let a = React.render(<Foo />, scratch);
expect(a.state).to.eql({
a: true,
b: true,
c: true
});
});
// Disabled to save bytes
xit('should throw an error for duplicate keys', () => {
const Foo = React.createClass({
mixins: [
{ getInitialState: () => ({ a: true }) }
],
getInitialState() {
return { a: true };
},
render() {
return <div />;
}
});
expect(() => {
React.render(<Foo />, scratch);
}).to.throw();
});
});
});
describe('refs', () => {
it('should support string refs', () => {
let inst, innerInst;
class Foo extends React.Component {
constructor() {
super();
inst = this;
}
render() {
return (
<div ref="top">
<h1 ref="h1">h1</h1>
<p ref="p">
<span ref="span">text</span>
<Inner ref="inner" />
</p>
</div>
);
}
}
const Inner = React.createClass({
render() {
innerInst = this;
return (
<div className="contained" ref="contained">
<h2 ref="inner-h2">h2</h2>
</div>
);
}
});
React.render(<Foo />, scratch);
expect(inst).to.exist;
expect(inst.refs).to.be.an.object;
expect(inst.refs).to.have.property('top', scratch.firstChild);
expect(inst.refs).to.have.property('h1', scratch.querySelector('h1'));
expect(inst.refs).to.have.property('p', scratch.querySelector('p'));
expect(inst.refs).to.have.property('span', scratch.querySelector('span'));
expect(inst.refs).to.have.property('inner', innerInst, 'ref to child component');
expect(inst.refs).not.to.have.property('contained');
expect(inst.refs).not.to.have.property('inner-h2');
expect(innerInst.refs).to.have.all.keys(['contained', 'inner-h2']);
expect(innerInst.refs).to.have.property('contained', scratch.querySelector('.contained'));
expect(innerInst.refs).to.have.property('inner-h2', scratch.querySelector('h2'));
});
it('should retain support for function refs', () => {
let ref1 = sinon.spy(),
ref2 = sinon.spy(),
componentRef = sinon.spy(),
innerInst;
class Foo extends React.Component {
render() {
return this.props.empty ? (<foo />) : (
<div ref={ref1}>
<h1 ref={ref2}>h1</h1>
<Inner ref={componentRef} />
</div>
);
}
}
const Inner = React.createClass({
render() {
innerInst = this;
return <div />;
}
});
React.render(<Foo />, scratch);
expect(ref1).to.have.have.been.calledOnce.and.calledWith(scratch.firstChild);
expect(ref2).to.have.have.been.calledOnce.and.calledWith(scratch.querySelector('h1'));
expect(componentRef).to.have.been.calledOnce.and.calledWith(innerInst);
React.render(<Foo empty />, scratch);
// React.unmountComponentAtNode(scratch);
expect(ref1, 'ref1').to.have.have.been.calledTwice.and.calledWith(null);
expect(ref2, 'ref2').to.have.have.been.calledTwice.and.calledWith(null);
expect(componentRef, 'componentRef').to.have.been.calledTwice.and.calledWith(null);
});
it('should support string refs via cloneElement()', () => {
let inner, outer;
const Inner = React.createClass({
render() {
inner = this;
return (
<div>
{React.cloneElement(React.Children.only(this.props.children), { id:'one' })}
{React.cloneElement(React.Children.only(this.props.children), { id:'two', ref:'two' })}
</div>
);
}
});
const Outer = React.createClass({
render() {
outer = this;
return (
<Inner>
<span ref="one">foo</span>
</Inner>
);
}
});
React.render(<Outer />, scratch);
let one = scratch.firstElementChild.children[0];
let two = scratch.firstElementChild.children[1];
expect(outer).to.have.property('refs').eql({ one });
expect(inner).to.have.property('refs').eql({ two });
});
});
describe('PureComponent', () => {
it('should be a class', () => {
expect(React).to.have.property('PureComponent').that.is.a('function');
});
it('should only re-render when props or state change', () => {
class C extends React.PureComponent {
render() {
return <div />;
}
}
let spy = sinon.spy(C.prototype, 'render');
let inst = React.render(<C />, scratch);
expect(spy).to.have.been.calledOnce;
spy.reset();
inst = React.render(<C />, scratch);
expect(spy).not.to.have.been.called;
let b = { foo: 'bar' };
inst = React.render(<C a="a" b={b} />, scratch);
expect(spy).to.have.been.calledOnce;
spy.reset();
inst = React.render(<C a="a" b={b} />, scratch);
expect(spy).not.to.have.been.called;
inst.setState({ });
rerender();
expect(spy).not.to.have.been.called;
inst.setState({ a:'a', b });
rerender();
expect(spy).to.have.been.calledOnce;
spy.reset();
inst.setState({ a:'a', b });
rerender();
expect(spy).not.to.have.been.called;
});
});
});