diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-11-08 15:07:07 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-11-08 15:07:07 +0100 |
commit | 6e5fb04d3f3f9a6cd43ac20896d73321dd079f96 (patch) | |
tree | 6e9f4002ba1f682eb2dca0c320ab1e9933665a33 /test/browser/devtools.js | |
parent | 30b577138dda685f65a8529be1866afa6e321845 (diff) |
Squashed 'thirdparty/preact/' changes from b2d90cc..ba094e2
ba094e2 Run only local tests for pull requests (#390)
e9fc3c2 Fix CI build (#386)
70a5ca3 This adds a link to preact-i18nline. (#382)
5dffd85 Merge branch 'pr-fix-build-for-windows' of https://github.com/Download/preact
f14edf7 kilobits => kilobytes (#383)
c193547 Test for #292
284e4aa 6.4.0
24eab2f Prevent accidental duplicate recycling of elements when swapping the base element of a component. Fixes #373.
76c5ef7 fix lint error
8008886 When swapping the base of a composed child component, update its parent's base reference. Fixes #349.
fd4f21f Add an equality check prior to setting `.nodeValue` on text nodes, since Firefox (and maybe others) don't do this internally by default. Fixes #368 - thanks @zbinlin!
1555e2b Add CDNJS version badge in readme (#365)
79c8bae Disable React Developer Tools integration tests under IE (#362)
84f4eeb Refactor `linkState()` a bit to drop around 40 bytes. Coincidentally, that's the exact size of the hooks just added for DevTools... 👌
22bbfcb Little tweaks 👯
f8b326e Document how to use the React DevTools with Preact (#354)
1f4a8eb Correct "preact/devtools" type definitions (#355)
68f22eb Add React Developer Tools integration (#339)
2a7a027 Add ref and allow objects in className (#316)
4a59cca fix readme todomvc link (#345)
37ca4e0 Fixes build for Windows #343
cf93387 6.3.0
ff05818 Make `VNode.children` *always* be an Array, even when there are no children.
9b095f4 Added link to preact-layout (#342)
git-subtree-dir: thirdparty/preact
git-subtree-split: ba094e27b602cb16aded7dcad95f71e44b7b0476
Diffstat (limited to 'test/browser/devtools.js')
-rw-r--r-- | test/browser/devtools.js | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/test/browser/devtools.js b/test/browser/devtools.js new file mode 100644 index 000000000..12c0e3369 --- /dev/null +++ b/test/browser/devtools.js @@ -0,0 +1,234 @@ +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); + }); +}); |