You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
15 KiB
543 lines
15 KiB
7 years ago
|
import { render, shallowRender } from '../src';
|
||
|
import { h, Component } from 'preact';
|
||
|
import chai, { expect } from 'chai';
|
||
|
import { spy, stub, match } from 'sinon';
|
||
|
import sinonChai from 'sinon-chai';
|
||
|
chai.use(sinonChai);
|
||
|
|
||
|
describe('render', () => {
|
||
|
describe('Basic JSX', () => {
|
||
|
it('should render JSX', () => {
|
||
|
let rendered = render(<div class="foo">bar</div>),
|
||
|
expected = `<div class="foo">bar</div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
describe('whitespace', () => {
|
||
|
it('should omit whitespace between elements', () => {
|
||
|
let children = [];
|
||
|
for (let i=0; i<1000; i++) {
|
||
|
children.push(Math.random()>.5 ? String(i) : h('x-'+String(i), null, i));
|
||
|
}
|
||
|
let rendered = render(
|
||
|
<div class="foo">
|
||
|
x
|
||
|
<a>a</a>
|
||
|
<b>b</b>
|
||
|
c
|
||
|
{children}
|
||
|
d
|
||
|
</div>
|
||
|
);
|
||
|
|
||
|
expect(rendered).not.to.contain(/\s/);
|
||
|
});
|
||
|
|
||
|
it('should not indent when attributes contain newlines', () => {
|
||
|
let rendered = render(
|
||
|
<div class={`foo\n\tbar\n\tbaz`}>
|
||
|
<a>a</a>
|
||
|
<b>b</b>
|
||
|
c
|
||
|
</div>
|
||
|
);
|
||
|
|
||
|
expect(rendered).to.equal(`<div class="foo\n\tbar\n\tbaz"><a>a</a><b>b</b>c</div>`);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should omit falsey attributes', () => {
|
||
|
let rendered = render(<div a={null} b={undefined} c={false} />),
|
||
|
expected = `<div></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
|
||
|
expect(render(<div foo={0} />)).to.equal(`<div foo="0"></div>`);
|
||
|
});
|
||
|
|
||
|
it('should collapse collapsible attributes', () => {
|
||
|
let rendered = render(<div class="" style="" foo={true} bar />),
|
||
|
expected = `<div class style foo bar></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should omit functions', () => {
|
||
|
let rendered = render(<div a={()=>{}} b={function(){}} />),
|
||
|
expected = `<div></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should encode entities', () => {
|
||
|
let rendered = render(<div a={'"<>&'}>{'"<>&'}</div>),
|
||
|
expected = `<div a=""<>&">"<>&</div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should omit falsey children', () => {
|
||
|
let rendered = render(<div>{null}|{undefined}|{false}</div>),
|
||
|
expected = `<div>||</div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should self-close void elements', () => {
|
||
|
let rendered = render(<div><input type='text' /><wbr /></div>),
|
||
|
expected = `<div><input type="text" /><wbr /></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('does not close void elements with closing tags', () => {
|
||
|
let rendered = render(<input><p>Hello World</p></input>),
|
||
|
expected = `<input /><p>Hello World</p>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should serialize object styles', () => {
|
||
|
let rendered = render(<div style={{ color:'red', border:'none' }} />),
|
||
|
expected = `<div style="color: red; border: none;"></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should ignore empty object styles', () => {
|
||
|
let rendered = render(<div style={{}} />),
|
||
|
expected = `<div></div>`;
|
||
|
|
||
|
expect(rendered).to.equal(expected);
|
||
|
});
|
||
|
|
||
|
it('should render SVG elements', () => {
|
||
|
let rendered = render((
|
||
|
<svg>
|
||
|
<image xlinkHref="#" />
|
||
|
<foreignObject>
|
||
|
<div xlinkHref="#" />
|
||
|
</foreignObject>
|
||
|
<g>
|
||
|
<image xlinkHref="#" />
|
||
|
</g>
|
||
|
</svg>
|
||
|
));
|
||
|
|
||
|
expect(rendered).to.equal(`<svg><image xlink:href="#"></image><foreignObject><div xlinkHref="#"></div></foreignObject><g><image xlink:href="#"></image></g></svg>`);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Functional Components', () => {
|
||
|
it('should render functional components', () => {
|
||
|
let Test = spy( ({ foo, children }) => <div foo={foo}>{ children }</div> );
|
||
|
|
||
|
let rendered = render(<Test foo="test">content</Test>);
|
||
|
|
||
|
expect(rendered)
|
||
|
.to.equal(`<div foo="test">content</div>`);
|
||
|
|
||
|
expect(Test)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.calledWithExactly(
|
||
|
match({
|
||
|
foo: 'test',
|
||
|
children: ['content']
|
||
|
}),
|
||
|
match({})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should render functional components within JSX', () => {
|
||
|
let Test = spy( ({ foo, children }) => <div foo={foo}>{ children }</div> );
|
||
|
|
||
|
let rendered = render(
|
||
|
<section>
|
||
|
<Test foo={1}><span>asdf</span></Test>
|
||
|
</section>
|
||
|
);
|
||
|
|
||
|
expect(rendered)
|
||
|
.to.equal(`<section><div foo="1"><span>asdf</span></div></section>`);
|
||
|
|
||
|
expect(Test)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.calledWithExactly(
|
||
|
match({
|
||
|
foo: 1,
|
||
|
children: [
|
||
|
match({ nodeName:'span', children:['asdf'] })
|
||
|
]
|
||
|
}),
|
||
|
match({})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should apply defaultProps', () => {
|
||
|
const Test = props => <div {...props} />;
|
||
|
Test.defaultProps = {
|
||
|
foo: 'default foo',
|
||
|
bar: 'default bar'
|
||
|
};
|
||
|
|
||
|
expect(render(<Test />), 'defaults').to.equal('<div foo="default foo" bar="default bar"></div>');
|
||
|
expect(render(<Test bar="b" />), 'partial').to.equal('<div foo="default foo" bar="b"></div>');
|
||
|
expect(render(<Test foo="a" bar="b" />), 'overridden').to.equal('<div foo="a" bar="b"></div>');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Classical Components', () => {
|
||
|
it('should render classical components', () => {
|
||
|
let Test = spy(class Test extends Component {
|
||
|
render({ foo, children }, state) {
|
||
|
return <div foo={foo}>{ children }</div>;
|
||
|
}
|
||
|
});
|
||
|
spy(Test.prototype, 'render');
|
||
|
|
||
|
let rendered = render(<Test foo="test">content</Test>);
|
||
|
|
||
|
const PROPS = {
|
||
|
foo: 'test',
|
||
|
children: ['content']
|
||
|
};
|
||
|
|
||
|
expect(rendered)
|
||
|
.to.equal(`<div foo="test">content</div>`);
|
||
|
|
||
|
expect(Test)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.calledWith(match(PROPS), match({}));
|
||
|
|
||
|
expect(Test.prototype.render)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.calledWithExactly(
|
||
|
match(PROPS),
|
||
|
match({}), // empty state
|
||
|
match({}) // empty context
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should render classical components within JSX', () => {
|
||
|
let Test = spy(class Test extends Component {
|
||
|
render({ foo, children }, state) {
|
||
|
return <div foo={foo}>{ children }</div>;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
spy(Test.prototype, 'render');
|
||
|
|
||
|
let rendered = render(
|
||
|
<section>
|
||
|
<Test foo={1}><span>asdf</span></Test>
|
||
|
</section>
|
||
|
);
|
||
|
|
||
|
expect(rendered)
|
||
|
.to.equal(`<section><div foo="1"><span>asdf</span></div></section>`);
|
||
|
|
||
|
expect(Test).to.have.been.calledOnce;
|
||
|
|
||
|
expect(Test.prototype.render)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.calledWithExactly(
|
||
|
match({
|
||
|
foo: 1,
|
||
|
children: [
|
||
|
match({ nodeName:'span', children:['asdf'] })
|
||
|
]
|
||
|
}),
|
||
|
match({}), // empty state
|
||
|
match({})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should apply defaultProps', () => {
|
||
|
class Test extends Component {
|
||
|
static defaultProps = {
|
||
|
foo: 'default foo',
|
||
|
bar: 'default bar'
|
||
|
};
|
||
|
render(props) {
|
||
|
return <div {...props} />;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
expect(render(<Test />), 'defaults').to.equal('<div foo="default foo" bar="default bar"></div>');
|
||
|
expect(render(<Test bar="b" />), 'partial').to.equal('<div foo="default foo" bar="b"></div>');
|
||
|
expect(render(<Test foo="a" bar="b" />), 'overridden').to.equal('<div foo="a" bar="b"></div>');
|
||
|
});
|
||
|
|
||
|
it('should invoke componentWillMount', () => {
|
||
|
class Test extends Component {
|
||
|
componentWillMount() {}
|
||
|
render(props) {
|
||
|
return <div {...props} />;
|
||
|
}
|
||
|
}
|
||
|
spy(Test.prototype, 'componentWillMount');
|
||
|
spy(Test.prototype, 'render');
|
||
|
|
||
|
render(<Test />);
|
||
|
|
||
|
expect(Test.prototype.componentWillMount)
|
||
|
.to.have.been.calledOnce
|
||
|
.and.to.have.been.calledBefore(Test.prototype.render);
|
||
|
});
|
||
|
|
||
|
it('should pass context to grandchildren', () => {
|
||
|
const CONTEXT = { a:'a' };
|
||
|
const PROPS = { b:'b' };
|
||
|
|
||
|
class Outer extends Component {
|
||
|
getChildContext() {
|
||
|
return CONTEXT;
|
||
|
}
|
||
|
render(props) {
|
||
|
return <div><Inner {...props} /></div>;
|
||
|
}
|
||
|
}
|
||
|
spy(Outer.prototype, 'getChildContext');
|
||
|
|
||
|
class Inner extends Component {
|
||
|
render(props, state, context) {
|
||
|
return <div>{ context && context.a }</div>;
|
||
|
}
|
||
|
}
|
||
|
spy(Inner.prototype, 'render');
|
||
|
|
||
|
render(<Outer />);
|
||
|
|
||
|
expect(Outer.prototype.getChildContext).to.have.been.calledOnce;
|
||
|
expect(Inner.prototype.render).to.have.been.calledWith(match({}), {}, CONTEXT);
|
||
|
|
||
|
CONTEXT.foo = 'bar';
|
||
|
render(<Outer {...PROPS} />);
|
||
|
|
||
|
expect(Outer.prototype.getChildContext).to.have.been.calledTwice;
|
||
|
expect(Inner.prototype.render).to.have.been.calledWith(match(PROPS), {}, CONTEXT);
|
||
|
});
|
||
|
|
||
|
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} />;
|
||
|
}
|
||
|
}
|
||
|
spy(Outer.prototype, 'getChildContext');
|
||
|
|
||
|
class Inner extends Component {
|
||
|
render(props, state, context) {
|
||
|
return <div>{ context && context.a }</div>;
|
||
|
}
|
||
|
}
|
||
|
spy(Inner.prototype, 'render');
|
||
|
|
||
|
render(<Outer />);
|
||
|
|
||
|
expect(Outer.prototype.getChildContext).to.have.been.calledOnce;
|
||
|
expect(Inner.prototype.render).to.have.been.calledWith(match({}), {}, CONTEXT);
|
||
|
|
||
|
CONTEXT.foo = 'bar';
|
||
|
render(<Outer {...PROPS} />);
|
||
|
|
||
|
expect(Outer.prototype.getChildContext).to.have.been.calledTwice;
|
||
|
expect(Inner.prototype.render).to.have.been.calledWith(match(PROPS), {}, CONTEXT);
|
||
|
|
||
|
// make sure render() could make use of context.a
|
||
|
expect(Inner.prototype.render).to.have.returned(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>;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
spy(Inner.prototype, 'render');
|
||
|
spy(InnerMost.prototype, 'render');
|
||
|
|
||
|
render(<Outer />);
|
||
|
|
||
|
expect(Inner.prototype.render).to.have.been.calledWith(match({}), {}, { outerContext });
|
||
|
expect(InnerMost.prototype.render).to.have.been.calledWith(match({}), {}, { outerContext, innerContext });
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('High-order components', () => {
|
||
|
class Outer extends Component {
|
||
|
render({ children, ...props }) {
|
||
|
return <Inner {...props} a="b">child <span>{ children }</span></Inner>;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Inner extends Component {
|
||
|
render({ children, ...props }) {
|
||
|
return <div id="inner" {...props} b="c" c="d">{ children }</div>;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
it('should resolve+render high order components', () => {
|
||
|
let rendered = render(<Outer a="a" b="b" p={1}>foo</Outer>);
|
||
|
expect(rendered).to.equal('<div id="inner" a="b" b="c" p="1" c="d">child <span>foo</span></div>');
|
||
|
});
|
||
|
|
||
|
it('should render child inline when shallow=true', () => {
|
||
|
let rendered = shallowRender(<Outer a="a" b="b" p={1}>foo</Outer>);
|
||
|
expect(rendered).to.equal('<Inner a="b" b="b" p="1">child <span>foo</span></Inner>');
|
||
|
});
|
||
|
|
||
|
it('should render nested high order components when shallowHighOrder=false', () => {
|
||
|
// using functions for meaningful generation of displayName
|
||
|
function Outer() { return <Middle />; }
|
||
|
function Middle() { return <div><Inner /></div>; }
|
||
|
function Inner() { return 'hi'; }
|
||
|
|
||
|
let rendered = render(<Outer />);
|
||
|
expect(rendered).to.equal('<div>hi</div>');
|
||
|
|
||
|
rendered = render(<Outer />, null, { shallow:true });
|
||
|
expect(rendered, '{shallow:true}').to.equal('<Middle></Middle>');
|
||
|
|
||
|
rendered = render(<Outer />, null, { shallow:true, shallowHighOrder:false });
|
||
|
expect(rendered, '{shallow:true,shallowHighOrder:false}').to.equal('<div><Inner></Inner></div>', 'but it should never render nested grandchild components');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('dangerouslySetInnerHTML', () => {
|
||
|
it('should support dangerouslySetInnerHTML', () => {
|
||
|
// some invalid HTML to make sure we're being flakey:
|
||
|
let html = '<a href="foo">asdf</a> some text <ul><li>foo<li>bar</ul>';
|
||
|
let rendered = render(<div id="f" dangerouslySetInnerHTML={{__html:html}} />);
|
||
|
expect(rendered).to.equal(`<div id="f">${html}</div>`);
|
||
|
});
|
||
|
|
||
|
it('should override children', () => {
|
||
|
let rendered = render(<div dangerouslySetInnerHTML={{__html:'foo'}}><b>bar</b></div>);
|
||
|
expect(rendered).to.equal('<div>foo</div>');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('className / class massaging', () => {
|
||
|
it('should render class using className', () => {
|
||
|
let rendered = render(<div className="foo bar" />);
|
||
|
expect(rendered).to.equal('<div class="foo bar"></div>');
|
||
|
});
|
||
|
|
||
|
it('should render class using class', () => {
|
||
|
let rendered = render(<div class="foo bar" />);
|
||
|
expect(rendered).to.equal('<div class="foo bar"></div>');
|
||
|
});
|
||
|
|
||
|
it('should prefer class over className', () => {
|
||
|
let rendered = render(<div class="foo" className="foo bar" />);
|
||
|
expect(rendered).to.equal('<div class="foo"></div>');
|
||
|
});
|
||
|
|
||
|
it('should stringify object classNames', () => {
|
||
|
let rendered = render(<div class={{ foo:1, bar:0, baz:true, buzz:false }} />);
|
||
|
expect(rendered, 'class').to.equal('<div class="foo baz"></div>');
|
||
|
|
||
|
rendered = render(<div className={{ foo:1, bar:0, baz:true, buzz:false }} />);
|
||
|
expect(rendered, 'className').to.equal('<div class="foo baz"></div>');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('sortAttributes', () => {
|
||
|
it('should leave attributes unsorted by default', () => {
|
||
|
let rendered = render(<div b1="b1" c="c" a="a" b="b" />);
|
||
|
expect(rendered).to.equal('<div b1="b1" c="c" a="a" b="b"></div>');
|
||
|
});
|
||
|
|
||
|
it('should sort attributes lexicographically if enabled', () => {
|
||
|
let rendered = render(<div b1="b1" c="c" a="a" b="b" />, null, { sortAttributes:true });
|
||
|
expect(rendered).to.equal('<div a="a" b="b" b1="b1" c="c"></div>');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('xml:true', () => {
|
||
|
let renderXml = jsx => render(jsx, null, { xml:true });
|
||
|
|
||
|
it('should render end-tags', () => {
|
||
|
expect(renderXml(<div />)).to.equal(`<div />`);
|
||
|
expect(renderXml(<a />)).to.equal(`<a />`);
|
||
|
expect(renderXml(<a>b</a>)).to.equal(`<a>b</a>`);
|
||
|
});
|
||
|
|
||
|
it('should render boolean attributes with named values', () => {
|
||
|
expect(renderXml(<div foo bar />)).to.equal(`<div foo="foo" bar="bar" />`);
|
||
|
});
|
||
|
|
||
|
it('should exclude falsey attributes', () => {
|
||
|
expect(renderXml(<div foo={false} bar={0} />)).to.equal(`<div bar="0" />`);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('state locking', () => {
|
||
|
it('should set _disable and __x to true', () => {
|
||
|
let inst;
|
||
|
class Foo extends Component {
|
||
|
constructor(props, context) {
|
||
|
super(props, context);
|
||
|
inst = this;
|
||
|
}
|
||
|
render() {
|
||
|
return <div />;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
expect(render(<Foo />)).to.equal('<div></div>');
|
||
|
|
||
|
expect(inst).to.have.property('_disable', true);
|
||
|
expect(inst).to.have.property('__x', true);
|
||
|
});
|
||
|
|
||
|
it('should prevent re-rendering', () => {
|
||
|
const Bar = stub().returns(<div />);
|
||
|
|
||
|
let count = 0;
|
||
|
|
||
|
class Foo extends Component {
|
||
|
componentWillMount() {
|
||
|
this.forceUpdate();
|
||
|
}
|
||
|
render() {
|
||
|
return <Bar count={++count} />;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
expect(render(<Foo />)).to.equal('<div></div>');
|
||
|
|
||
|
expect(Bar).to.have.been.calledOnce.and.calledWithMatch({ count: 1 });
|
||
|
});
|
||
|
});
|
||
|
});
|