235 lines
7.3 KiB
JavaScript
235 lines
7.3 KiB
JavaScript
|
import { h, Component, render } from '../../src/preact';
|
||
|
import { initDevTools } from '../../devtools/devtools';
|
||
|
import { unmountComponent } from '../../src/vdom/component';
|
||
|
|
||
|
class StatefulComponent extends Component {
|
||
|
constructor(props) {
|
||
|
super(props);
|
||
|
|
||
|
this.state = {count: 0};
|
||
|
}
|
||
|
|
||
|
render() {
|
||
|
return h('span', {}, String(this.state.count));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function FunctionalComponent() {
|
||
|
return h('span', {class: 'functional'}, 'Functional');
|
||
|
}
|
||
|
|
||
|
function Label({label}) {
|
||
|
return label;
|
||
|
}
|
||
|
|
||
|
class MultiChild extends Component {
|
||
|
constructor(props) {
|
||
|
super(props);
|
||
|
this.state = {count: props.initialCount};
|
||
|
}
|
||
|
|
||
|
render() {
|
||
|
return h('div', {}, Array(this.state.count).fill('child'));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let describe_ = describe;
|
||
|
if (!('name' in Function.prototype)) {
|
||
|
// Skip these tests under Internet Explorer
|
||
|
describe_ = describe.skip;
|
||
|
}
|
||
|
|
||
|
describe_('React Developer Tools integration', () => {
|
||
|
let cleanup;
|
||
|
let container;
|
||
|
let renderer;
|
||
|
|
||
|
// Maps of DOM node to React*Component-like objects.
|
||
|
// For composite components, there will be two instances for each node, one
|
||
|
// for the composite component (instanceMap) and one for the root child DOM
|
||
|
// component rendered by that component (domInstanceMap)
|
||
|
let instanceMap = new Map();
|
||
|
let domInstanceMap = new Map();
|
||
|
|
||
|
beforeEach(() => {
|
||
|
container = document.createElement('div');
|
||
|
document.body.appendChild(container);
|
||
|
|
||
|
const onMount = instance => {
|
||
|
if (instance._renderedChildren) {
|
||
|
domInstanceMap.set(instance.node, instance);
|
||
|
} else {
|
||
|
instanceMap.set(instance.node, instance);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const onUnmount = instance => {
|
||
|
instanceMap.delete(instance.node);
|
||
|
domInstanceMap.delete(instance.node);
|
||
|
};
|
||
|
|
||
|
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
||
|
inject: sinon.spy(_renderer => {
|
||
|
renderer = _renderer;
|
||
|
renderer.Mount._renderNewRootComponent = sinon.stub();
|
||
|
renderer.Reconciler.mountComponent = sinon.spy(onMount);
|
||
|
renderer.Reconciler.unmountComponent = sinon.spy(onUnmount);
|
||
|
renderer.Reconciler.receiveComponent = sinon.stub();
|
||
|
})
|
||
|
};
|
||
|
cleanup = initDevTools();
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
container.remove();
|
||
|
cleanup();
|
||
|
});
|
||
|
|
||
|
it('registers preact as a renderer with the React DevTools hook', () => {
|
||
|
expect(global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject).to.be.called;
|
||
|
});
|
||
|
|
||
|
// Basic component addition/update/removal tests
|
||
|
it('notifies dev tools about new components', () => {
|
||
|
render(h(StatefulComponent), container);
|
||
|
expect(renderer.Reconciler.mountComponent).to.be.called;
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools about component updates', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
node._component.forceUpdate();
|
||
|
expect(renderer.Reconciler.receiveComponent).to.be.called;
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools when components are removed', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
unmountComponent(node._component, true);
|
||
|
expect(renderer.Reconciler.unmountComponent).to.be.called;
|
||
|
});
|
||
|
|
||
|
// Test properties of DOM components exposed to devtools via
|
||
|
// ReactDOMComponent-like instances
|
||
|
it('exposes the tag name of DOM components', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
const domInstance = domInstanceMap.get(node);
|
||
|
expect(domInstance._currentElement.type).to.equal('span');
|
||
|
});
|
||
|
|
||
|
it('exposes DOM component props', () => {
|
||
|
const node = render(h(FunctionalComponent), container);
|
||
|
const domInstance = domInstanceMap.get(node);
|
||
|
expect(domInstance._currentElement.props.class).to.equal('functional');
|
||
|
});
|
||
|
|
||
|
it('exposes text component contents', () => {
|
||
|
const node = render(h(Label, {label: 'Text content'}), container);
|
||
|
const textInstance = domInstanceMap.get(node);
|
||
|
expect(textInstance._stringText).to.equal('Text content');
|
||
|
});
|
||
|
|
||
|
// Test properties of composite components exposed to devtools via
|
||
|
// ReactCompositeComponent-like instances
|
||
|
it('exposes the name of composite component classes', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
expect(instanceMap.get(node).getName()).to.equal('StatefulComponent');
|
||
|
});
|
||
|
|
||
|
it('exposes composite component props', () => {
|
||
|
const node = render(h(Label, {label: 'Text content'}), container);
|
||
|
const instance = instanceMap.get(node);
|
||
|
expect(instance._currentElement.props.label).to.equal('Text content');
|
||
|
});
|
||
|
|
||
|
it('exposes composite component state', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
|
||
|
node._component.setState({count: 42});
|
||
|
node._component.forceUpdate();
|
||
|
|
||
|
expect(instanceMap.get(node).state).to.deep.equal({count: 42});
|
||
|
});
|
||
|
|
||
|
// Test setting state via devtools
|
||
|
it('updates component when setting state from devtools', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
|
||
|
instanceMap.get(node).setState({count: 10});
|
||
|
instanceMap.get(node).forceUpdate();
|
||
|
|
||
|
expect(node.textContent).to.equal('10');
|
||
|
});
|
||
|
|
||
|
// Test that the original instance is exposed via `_instance` so it can
|
||
|
// be accessed conveniently via `$r` in devtools
|
||
|
|
||
|
// Functional component handling tests
|
||
|
it('wraps functional components with stateful ones', () => {
|
||
|
const vnode = h(FunctionalComponent);
|
||
|
expect(vnode.nodeName.prototype).to.have.property('render');
|
||
|
});
|
||
|
|
||
|
it('exposes the name of functional components', () => {
|
||
|
const node = render(h(FunctionalComponent), container);
|
||
|
const instance = instanceMap.get(node);
|
||
|
expect(instance.getName()).to.equal('FunctionalComponent');
|
||
|
});
|
||
|
|
||
|
it('exposes a fallback name if the component has no useful name', () => {
|
||
|
const node = render(h(() => h('div')), container);
|
||
|
const instance = instanceMap.get(node);
|
||
|
expect(instance.getName()).to.equal('(Function.name missing)');
|
||
|
});
|
||
|
|
||
|
// Test handling of DOM children
|
||
|
it('notifies dev tools about DOM children', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
const domInstance = domInstanceMap.get(node);
|
||
|
expect(renderer.Reconciler.mountComponent).to.have.been.calledWith(domInstance);
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools when a component update adds DOM children', () => {
|
||
|
const node = render(h(MultiChild, {initialCount: 2}), container);
|
||
|
|
||
|
node._component.setState({count: 4});
|
||
|
node._component.forceUpdate();
|
||
|
|
||
|
expect(renderer.Reconciler.mountComponent).to.have.been.called.twice;
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools when a component update modifies DOM children', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
|
||
|
instanceMap.get(node).setState({count: 10});
|
||
|
instanceMap.get(node).forceUpdate();
|
||
|
|
||
|
const textInstance = domInstanceMap.get(node.childNodes[0]);
|
||
|
expect(textInstance._stringText).to.equal('10');
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools when a component update removes DOM children', () => {
|
||
|
const node = render(h(MultiChild, {initialCount: 1}), container);
|
||
|
|
||
|
node._component.setState({count: 0});
|
||
|
node._component.forceUpdate();
|
||
|
|
||
|
expect(renderer.Reconciler.unmountComponent).to.be.called;
|
||
|
});
|
||
|
|
||
|
// Root component info
|
||
|
it('exposes root components on the _instancesByReactRootID map', () => {
|
||
|
render(h(StatefulComponent), container);
|
||
|
expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1);
|
||
|
});
|
||
|
|
||
|
it('notifies dev tools when new root components are mounted', () => {
|
||
|
render(h(StatefulComponent), container);
|
||
|
expect(renderer.Mount._renderNewRootComponent).to.be.called;
|
||
|
});
|
||
|
|
||
|
it('removes root components when they are unmounted', () => {
|
||
|
const node = render(h(StatefulComponent), container);
|
||
|
unmountComponent(node._component, true);
|
||
|
expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(0);
|
||
|
});
|
||
|
});
|