Squashed 'thirdparty/preact/' changes from b2d90cc..ba094e2

ba094e2 Run only local tests for pull requests ()
e9fc3c2 Fix CI build ()
70a5ca3 This adds a link to preact-i18nline. ()
5dffd85 Merge branch 'pr-fix-build-for-windows' of https://github.com/Download/preact
f14edf7 kilobits => kilobytes ()
c193547 Test for 
284e4aa 6.4.0
24eab2f Prevent accidental duplicate recycling of elements when swapping the base element of a component. Fixes .
76c5ef7 fix lint error
8008886 When swapping the base of a composed child component, update its parent's base reference. Fixes .
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  - thanks @zbinlin!
1555e2b Add CDNJS version badge in readme ()
79c8bae Disable React Developer Tools integration tests under IE ()
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 ()
1f4a8eb Correct "preact/devtools" type definitions ()
68f22eb Add React Developer Tools integration ()
2a7a027 Add ref and allow objects in className ()
4a59cca fix readme todomvc link ()
37ca4e0 Fixes build for Windows 
cf93387 6.3.0
ff05818 Make `VNode.children` *always* be an Array, even when there are no children.
9b095f4 Added link to preact-layout ()

git-subtree-dir: thirdparty/preact
git-subtree-split: ba094e27b602cb16aded7dcad95f71e44b7b0476
This commit is contained in:
Florian Dold 2016-11-08 15:07:07 +01:00
parent 30b577138d
commit 6e5fb04d3f
25 changed files with 979 additions and 153 deletions

6
.gitignore vendored
View File

@ -4,5 +4,7 @@
/dist /dist
/_dev /_dev
/coverage /coverage
aliases.js
aliases.js.map # Additional bundles
/*.js
/*.js.map

View File

@ -9,13 +9,19 @@ cache:
directories: directories:
- node_modules - node_modules
# Make chrome browser available for testing
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
install: install:
- npm install - npm install
script: script:
- npm run build - npm run build
- npm run test - npm run test
- SAUCELABS=true COVERAGE=false FLAKEY=false PERFORMANCE=false npm run test:karma - BROWSER=true COVERAGE=false FLAKEY=false PERFORMANCE=false npm run test:karma
# Necessary to compile native modules for io.js v3 or Node.js v4 # Necessary to compile native modules for io.js v3 or Node.js v4
env: env:

View File

@ -2,13 +2,14 @@
<img alt="Preact" title="Preact" src="https://cdn.rawgit.com/developit/b4416d5c92b743dbaec1e68bc4c27cda/raw/3235dc508f7eb834ebf48418aea212a05df13db1/preact-logo-trans.svg" width="550"> <img alt="Preact" title="Preact" src="https://cdn.rawgit.com/developit/b4416d5c92b743dbaec1e68bc4c27cda/raw/3235dc508f7eb834ebf48418aea212a05df13db1/preact-logo-trans.svg" width="550">
</a> </a>
**Preact is a fast, `3kb` alternative to React, with the same ES2015 API.** **Preact is a fast, `3kB` alternative to React, with the same ES2015 API.**
Preact retains a large amount of compatibility with React, but only the modern ([ES6 Classes] and [stateless functional components](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components)) interfaces. Preact retains a large amount of compatibility with React, but only the modern ([ES6 Classes] and [stateless functional components](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components)) interfaces.
As one would expect coming from React, Components are simple building blocks for composing a User Interface. As one would expect coming from React, Components are simple building blocks for composing a User Interface.
### :information_desk_person: Full documentation is available at the [Preact Website ➞](https://preactjs.com) ### :information_desk_person: Full documentation is available at the [Preact Website ➞](https://preactjs.com)
[![CDNJS](https://img.shields.io/cdnjs/v/preact.svg)](https://cdnjs.com/libraries/preact)
[![npm](https://img.shields.io/npm/v/preact.svg)](http://npm.im/preact) [![npm](https://img.shields.io/npm/v/preact.svg)](http://npm.im/preact)
[![travis](https://travis-ci.org/developit/preact.svg?branch=master)](https://travis-ci.org/developit/preact) [![travis](https://travis-ci.org/developit/preact.svg?branch=master)](https://travis-ci.org/developit/preact)
[![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/developit/preact) [![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/developit/preact)
@ -24,7 +25,7 @@ As one would expect coming from React, Components are simple building blocks for
- [**ESBench**](http://esbench.com) is built using Preact. - [**ESBench**](http://esbench.com) is built using Preact.
- [**Nectarine.rocks**](http://nectarine.rocks) _([Github Project](https://github.com/developit/nectarine))_ :peach: - [**Nectarine.rocks**](http://nectarine.rocks) _([Github Project](https://github.com/developit/nectarine))_ :peach:
- [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([Github Project](https://github.com/developit/documentation-viewer))_ - [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([Github Project](https://github.com/developit/documentation-viewer))_
- [**TodoMVC**](http://developit.github.io/preact-todomvc/) _([Github Project](https://github.com/developit/preact-todomvc))_ - [**TodoMVC**](https://preact-todomvc.surge.sh) _([Github Project](https://github.com/developit/preact-todomvc))_
- [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([Github Project](https://github.com/developit/hn_minimal))_ - [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([Github Project](https://github.com/developit/hn_minimal))_
- [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([Github Project](https://github.com/developit/preact-boilerplate))_ :zap: - [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([Github Project](https://github.com/developit/preact-boilerplate))_ :zap:
- [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star: - [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star:
@ -41,17 +42,25 @@ As one would expect coming from React, Components are simple building blocks for
## Libraries & Add-ons ## Libraries & Add-ons
- :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components. - :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))*
- :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact
- :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering. - :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering.
- :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact. *([full example](http://git.io/preact-compat-example))* - :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components
- :rocket: [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com).
- :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_
- :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components - :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components
- :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component
- :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact.
- :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way: - :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way:
- :construction: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): A utility method to create components without the `class` keyword - :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component
- :bookmark: [**preact-token-input**](https://github.com/developit/preact-token-input): Text field that tokenizes input, for things like tags
- :card_index: [**preact-virtual-list**](https://github.com/developit/preact-virtual-list): Easily render lists with millions of rows ([demo](https://jsfiddle.net/developit/qqan9pdo/))
- :triangular_ruler: [**preact-layout**](https://download.github.io/preact-layout/): Small and simple layout library
- :thought_balloon: [**preact-socrates**](https://github.com/matthewmueller/preact-socrates): Preact plugin for [Socrates](http://github.com/matthewmueller/socrates)
- :rowboat: [**preact-flyd**](https://github.com/xialvjun/preact-flyd): Use [flyd](https://github.com/paldepind/flyd) FRP streams in Preact + JSX
- :speech_balloon: [**preact-i18nline**](https://github.com/download/preact-i18nline): Integrates the ecosystem around [i18n-js](https://github.com/everydayhero/i18n-js) with Preact via [i18nline](https://github.com/download/i18nline).
- :white_square_button: [**preact-mdl**](https://git.io/preact-mdl): Use [MDL](https://getmdl.io) as Preact components
- :rocket: [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com)
- :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_
- :tophat: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): create preact components without the class keyword
- :hammer: [**preact-hyperscript**](https://github.com/queckezz/preact-hyperscript): Hyperscript-like syntax for creating elements - :hammer: [**preact-hyperscript**](https://github.com/queckezz/preact-hyperscript): Hyperscript-like syntax for creating elements
- :white_check_mark: [**shallow-compare**](https://github.com/tkh44/shallow-compare): simplified `shouldComponentUpdate` helper.
## Getting Started ## Getting Started
@ -328,6 +337,24 @@ class MixedComponent extends Component {
} }
``` ```
## Developer Tools
You can inspect and modify the state of your Preact UI components at runtime using the
[React Developer Tools](https://github.com/facebook/react-devtools) browser extension.
1. Install the [React Developer Tools](https://github.com/facebook/react-devtools) extension
2. Import the "preact/devtools" module in your app
3. Reload and go to the 'React' tab in the browser's development tools
```js
import { h, Component, render } from 'preact';
// Enable devtools. You can reduce the size of your app by only including this
// module in development builds. eg. In Webpack, wrap this with an `if (module.hot) {...}`
// check.
require('preact/devtools');
```
## License ## License

View File

@ -0,0 +1,20 @@
import nodeResolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
export default {
entry: 'devtools/index.js',
external: ['preact'],
format: 'umd',
globals: {
preact: 'preact'
},
moduleName: 'preactDevTools',
plugins: [
babel({
sourceMap: true,
loose: 'all',
blacklist: ['es6.tailCall'],
exclude: 'node_modules/**'
})
]
}

427
devtools/devtools.js Normal file
View File

@ -0,0 +1,427 @@
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
import { options, Component } from 'preact';
// Internal helpers from preact
import { ATTR_KEY } from '../src/constants';
import { isFunctionalComponent } from '../src/vdom/functional-component';
/**
* Return a ReactElement-compatible object for the current state of a preact
* component.
*/
function createReactElement(component) {
return {
type: component.constructor,
key: component.key,
ref: null, // Unsupported
props: component.props
};
}
/**
* Create a ReactDOMComponent-compatible object for a given DOM node rendered
* by preact.
*
* This implements the subset of the ReactDOMComponent interface that
* React DevTools requires in order to display DOM nodes in the inspector with
* the correct type and properties.
*
* @param {Node} node
*/
function createReactDOMComponent(node) {
const childNodes = node.nodeType === Node.ELEMENT_NODE ?
Array.from(node.childNodes) : [];
const isText = node.nodeType === Node.TEXT_NODE;
return {
// --- ReactDOMComponent interface
_currentElement: isText ? node.textContent : {
type: node.nodeName.toLowerCase(),
props: node[ATTR_KEY]
},
_renderedChildren: childNodes.map(child => {
if (child._component) {
return updateReactComponent(child._component);
}
return updateReactComponent(child);
}),
_stringText: isText ? node.textContent : null,
// --- Additional properties used by preact devtools
// A flag indicating whether the devtools have been notified about the
// existence of this component instance yet.
// This is used to send the appropriate notifications when DOM components
// are added or updated between composite component updates.
_inDevTools: false,
node
};
}
/**
* Return the name of a component created by a `ReactElement`-like object.
*
* @param {ReactElement} element
*/
function typeName(element) {
if (typeof element.type === 'function') {
return element.type.displayName || element.type.name;
}
return element.type;
}
/**
* Return a ReactCompositeComponent-compatible object for a given preact
* component instance.
*
* This implements the subset of the ReactCompositeComponent interface that
* the DevTools requires in order to walk the component tree and inspect the
* component's properties.
*
* See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js
*/
function createReactCompositeComponent(component) {
const _currentElement = createReactElement(component);
const node = component.base;
let instance = {
// --- ReactDOMComponent properties
getName() {
return typeName(_currentElement);
},
_currentElement: createReactElement(component),
props: component.props,
state: component.state,
forceUpdate: component.forceUpdate.bind(component),
setState: component.setState.bind(component),
// --- Additional properties used by preact devtools
node
};
// React DevTools exposes the `_instance` field of the selected item in the
// component tree as `$r` in the console. `_instance` must refer to a
// React Component (or compatible) class instance with `props` and `state`
// fields and `setState()`, `forceUpdate()` methods.
instance._instance = component;
// If the root node returned by this component instance's render function
// was itself a composite component, there will be a `_component` property
// containing the child component instance.
if (component._component) {
instance._renderedComponent = updateReactComponent(component._component);
} else {
// Otherwise, if the render() function returned an HTML/SVG element,
// create a ReactDOMComponent-like object for the DOM node itself.
instance._renderedComponent = updateReactComponent(node);
}
return instance;
}
/**
* Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like
* object.
*
* The same React*Component instance must be used when notifying devtools
* about the initial mount of a component and subsequent updates.
*/
let instanceMap = new Map();
/**
* Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like
* instance for a given preact component instance or DOM Node.
*
* @param {Component|Node} componentOrNode
*/
function updateReactComponent(componentOrNode) {
const newInstance = componentOrNode instanceof Node ?
createReactDOMComponent(componentOrNode) :
createReactCompositeComponent(componentOrNode);
if (instanceMap.has(componentOrNode)) {
let inst = instanceMap.get(componentOrNode);
Object.assign(inst, newInstance);
return inst;
}
instanceMap.set(componentOrNode, newInstance);
return newInstance;
}
function nextRootKey(roots) {
return '.' + Object.keys(roots).length;
}
/**
* Find all root component instances rendered by preact in `node`'s children
* and add them to the `roots` map.
*
* @param {DOMElement} node
* @param {[key: string] => ReactDOMComponent|ReactCompositeComponent}
*/
function findRoots(node, roots) {
Array.from(node.childNodes).forEach(child => {
if (child._component) {
roots[nextRootKey(roots)] = updateReactComponent(child._component);
} else {
findRoots(child, roots);
}
});
}
/**
* Map of functional component name -> wrapper class.
*/
let functionalComponentWrappers = new Map();
/**
* Wrap a functional component with a stateful component.
*
* preact does not record any information about the original hierarchy of
* functional components in the rendered DOM nodes. Wrapping functional components
* with a trivial wrapper allows us to recover information about the original
* component structure from the DOM.
*
* @param {VNode} vnode
*/
function wrapFunctionalComponent(vnode) {
const originalRender = vnode.nodeName;
const name = vnode.nodeName.name || '(Function.name missing)';
const wrappers = functionalComponentWrappers;
if (!wrappers.has(originalRender)) {
let wrapper = class extends Component {
render(props, state, context) {
return originalRender(props, context);
}
};
// Expose the original component name. React Dev Tools will use
// this property if it exists or fall back to Function.name
// otherwise.
wrapper.displayName = name;
wrappers.set(originalRender, wrapper);
}
vnode.nodeName = wrappers.get(originalRender);
}
/**
* Create a bridge for exposing preact's component tree to React DevTools.
*
* It creates implementations of the interfaces that ReactDOM passes to
* devtools to enable it to query the component tree and hook into component
* updates.
*
* See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js
* for how ReactDOM exports its internals for use by the devtools and
* the `attachRenderer()` function in
* https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js
* for how the devtools consumes the resulting objects.
*/
function createDevToolsBridge() {
// The devtools has different paths for interacting with the renderers from
// React Native, legacy React DOM and current React DOM.
//
// Here we emulate the interface for the current React DOM (v15+) lib.
// ReactDOMComponentTree-like object
const ComponentTree = {
getNodeFromInstance(instance) {
return instance.node;
},
getClosestInstanceFromNode(node) {
while (node && !node._component) {
node = node.parentNode;
}
return node ? updateReactComponent(node._component) : null;
}
};
// Map of root ID (the ID is unimportant) to component instance.
let roots = {};
findRoots(document.body, roots);
// ReactMount-like object
//
// Used by devtools to discover the list of root component instances and get
// notified when new root components are rendered.
const Mount = {
_instancesByReactRootID: roots,
// Stub - React DevTools expects to find this method and replace it
// with a wrapper in order to observe new root components being added
_renderNewRootComponent(/* instance, ... */) { }
};
// ReactReconciler-like object
const Reconciler = {
// Stubs - React DevTools expects to find these methods and replace them
// with wrappers in order to observe components being mounted, updated and
// unmounted
mountComponent(/* instance, ... */) { },
performUpdateIfNecessary(/* instance, ... */) { },
receiveComponent(/* instance, ... */) { },
unmountComponent(/* instance, ... */) { }
};
/** Notify devtools that a new component instance has been mounted into the DOM. */
const componentAdded = component => {
const instance = updateReactComponent(component);
if (isRootComponent(component)) {
instance._rootID = nextRootKey(roots);
roots[instance._rootID] = instance;
Mount._renderNewRootComponent(instance);
}
visitNonCompositeChildren(instance, childInst => {
childInst._inDevTools = true;
Reconciler.mountComponent(childInst);
});
Reconciler.mountComponent(instance);
};
/** Notify devtools that a component has been updated with new props/state. */
const componentUpdated = component => {
const prevRenderedChildren = [];
visitNonCompositeChildren(instanceMap.get(component), childInst => {
prevRenderedChildren.push(childInst);
});
// Notify devtools about updates to this component and any non-composite
// children
const instance = updateReactComponent(component);
Reconciler.receiveComponent(instance);
visitNonCompositeChildren(instance, childInst => {
if (!childInst._inDevTools) {
// New DOM child component
childInst._inDevTools = true;
Reconciler.mountComponent(childInst);
} else {
// Updated DOM child component
Reconciler.receiveComponent(childInst);
}
});
// For any non-composite children that were removed by the latest render,
// remove the corresponding ReactDOMComponent-like instances and notify
// the devtools
prevRenderedChildren.forEach(childInst => {
if (!document.body.contains(childInst.node)) {
instanceMap.delete(childInst.node);
Reconciler.unmountComponent(childInst);
}
});
};
/** Notify devtools that a component has been unmounted from the DOM. */
const componentRemoved = component => {
const instance = updateReactComponent(component);
visitNonCompositeChildren(childInst => {
instanceMap.delete(childInst.node);
Reconciler.unmountComponent(childInst);
});
Reconciler.unmountComponent(instance);
instanceMap.delete(component);
if (instance._rootID) {
delete roots[instance._rootID];
}
};
return {
componentAdded,
componentUpdated,
componentRemoved,
// Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject()
ComponentTree,
Mount,
Reconciler
};
}
/**
* Return `true` if a preact component is a top level component rendered by
* `render()` into a container Element.
*/
function isRootComponent(component) {
return !component.base.parentElement || !component.base.parentElement[ATTR_KEY];
}
/**
* Visit all child instances of a ReactCompositeComponent-like object that are
* not composite components (ie. they represent DOM elements or text)
*
* @param {Component} component
* @param {(Component) => void} visitor
*/
function visitNonCompositeChildren(component, visitor) {
if (component._renderedComponent) {
if (!component._renderedComponent._component) {
visitor(component._renderedComponent);
visitNonCompositeChildren(component._renderedComponent, visitor);
}
} else if (component._renderedChildren) {
component._renderedChildren.forEach(child => {
visitor(child);
if (!child._component) visitNonCompositeChildren(child, visitor);
});
}
}
/**
* Create a bridge between the preact component tree and React's dev tools
* and register it.
*
* After this function is called, the React Dev Tools should be able to detect
* "React" on the page and show the component tree.
*
* This function hooks into preact VNode creation in order to expose functional
* components correctly, so it should be called before the root component(s)
* are rendered.
*
* Returns a cleanup function which unregisters the hooks.
*/
export function initDevTools() {
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
// React DevTools are not installed
return;
}
// Hook into preact element creation in order to wrap functional components
// with stateful ones in order to make them visible in the devtools
const nextVNode = options.vnode;
options.vnode = (vnode) => {
if (isFunctionalComponent(vnode)) wrapFunctionalComponent(vnode);
if (nextVNode) return nextVNode(vnode);
};
// Notify devtools when preact components are mounted, updated or unmounted
const bridge = createDevToolsBridge();
const nextAfterMount = options.afterMount;
options.afterMount = component => {
bridge.componentAdded(component);
if (nextAfterMount) nextAfterMount(component);
};
const nextAfterUpdate = options.afterUpdate;
options.afterUpdate = component => {
bridge.componentUpdated(component);
if (nextAfterUpdate) nextAfterUpdate(component);
};
const nextBeforeUnmount = options.beforeUnmount;
options.beforeUnmount = component => {
bridge.componentRemoved(component);
if (nextBeforeUnmount) nextBeforeUnmount(component);
};
// Notify devtools about this instance of "React"
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge);
return () => {
options.afterMount = nextAfterMount;
options.afterUpdate = nextAfterUpdate;
options.beforeUnmount = nextBeforeUnmount;
};
}

4
devtools/index.js Normal file
View File

@ -0,0 +1,4 @@
import { initDevTools } from './devtools';
initDevTools();

View File

@ -1,7 +1,7 @@
{ {
"name": "preact", "name": "preact",
"amdName": "preact", "amdName": "preact",
"version": "6.2.1", "version": "6.4.0",
"description": "Tiny & fast Component-based virtual DOM framework.", "description": "Tiny & fast Component-based virtual DOM framework.",
"main": "dist/preact.js", "main": "dist/preact.js",
"jsnext:main": "src/preact.js", "jsnext:main": "src/preact.js",
@ -9,25 +9,27 @@
"dev:main": "dist/preact.dev.js", "dev:main": "dist/preact.dev.js",
"minified:main": "dist/preact.min.js", "minified:main": "dist/preact.min.js",
"scripts": { "scripts": {
"clean": "rimraf dist/ $npm_package_aliases_main ${npm_package_aliases_main}.map", "clean": "rimraf dist/ aliases.js aliases.js.map devtools.js devtools.js.map",
"copy-flow-definition": "cp src/preact.js.flow dist/preact.js.flow", "copy-flow-definition": "copyfiles src/preact.js.flow dist/preact.js.flow",
"copy-typescript-definition": "cp src/preact.d.ts dist/preact.d.ts", "copy-typescript-definition": "copyfiles src/preact.d.ts dist/preact.d.ts",
"build": "npm-run-all --silent clean transpile copy-flow-definition copy-typescript-definition strip optimize minify size", "build": "npm-run-all --silent clean transpile copy-flow-definition copy-typescript-definition strip optimize minify size",
"transpile:main": "rollup -c config/rollup.config.js -m ${npm_package_dev_main}.map -f umd -n $npm_package_amdName $npm_package_jsnext_main -o $npm_package_dev_main", "transpile:main": "rollup -c config/rollup.config.js -m dist/preact.dev.js.map -f umd -n preact src/preact.js -o dist/preact.dev.js",
"transpile:aliases": "rollup -c config/rollup.config.aliases.js -m ${npm_package_aliases_main}.map -f umd -n $npm_package_amdName $npm_package_jsnext_main -o $npm_package_aliases_main", "transpile:devtools": "rollup -c config/rollup.config.devtools.js -o devtools.js -m devtools.js.map",
"transpile": "npm-run-all transpile:main transpile:aliases", "transpile:aliases": "rollup -c config/rollup.config.aliases.js -m aliases.js.map -f umd -n preact src/preact.js -o aliases.js",
"optimize": "uglifyjs $npm_package_dev_main -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty -b width=120,quote_style=3 -o $npm_package_main -p relative --in-source-map ${npm_package_dev_main}.map --source-map ${npm_package_main}.map", "transpile": "npm-run-all transpile:main transpile:aliases transpile:devtools",
"minify": "uglifyjs $npm_package_main -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map", "optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty -b width=120,quote_style=3 -o dist/preact.js -p relative --in-source-map dist/preact.dev.js.map --source-map dist/preact.js.map",
"minify": "uglifyjs dist/preact.js -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o dist/preact.min.js -p relative --in-source-map dist/preact.js.map --source-map dist/preact.min.js.map",
"strip": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.dev.js", "strip": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.dev.js",
"size": "size=$(gzip-size $npm_package_minified_main) && echo \"gzip size: $size / $(pretty-bytes $size)\"", "size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size dist/preact.min.js",
"test": "npm-run-all lint --parallel test:mocha test:karma", "test": "npm-run-all lint --parallel test:mocha test:karma",
"test:mocha": "mocha --recursive --compilers js:babel/register test/shared test/node", "test:mocha": "mocha --recursive --compilers js:babel/register test/shared test/node",
"test:karma": "karma start test/karma.conf.js --single-run", "test:karma": "karma start test/karma.conf.js --single-run",
"test:mocha:watch": "npm run test:mocha -- --watch", "test:mocha:watch": "npm run test:mocha -- --watch",
"test:karma:watch": "npm run test:karma -- no-single-run", "test:karma:watch": "npm run test:karma -- no-single-run",
"lint": "eslint src test", "lint": "eslint devtools src test",
"prepublish": "npm run build", "prepublish": "npm run build",
"release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" "smart-release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish",
"release": "cross-env npm run smart-release"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "./config/eslint-config.js" "extends": "./config/eslint-config.js"
@ -38,10 +40,13 @@
"url": "https://github.com/developit/preact.git" "url": "https://github.com/developit/preact.git"
}, },
"files": [ "files": [
"devtools",
"src", "src",
"dist", "dist",
"aliases.js", "aliases.js",
"aliases.js.map", "aliases.js.map",
"devtools.js",
"devtools.js.map",
"typings.json" "typings.json"
], ],
"author": "Jason Miller <jason@developit.ca>", "author": "Jason Miller <jason@developit.ca>",
@ -57,6 +62,9 @@
"babel-loader": "^5.3.2", "babel-loader": "^5.3.2",
"babel-runtime": "^5.8.24", "babel-runtime": "^5.8.24",
"chai": "^3.4.1", "chai": "^3.4.1",
"copyfiles": "^1.0.0",
"core-js": "^2.4.1",
"cross-env": "^3.1.3",
"diff": "^3.0.0", "diff": "^3.0.0",
"eslint": "^3.0.0", "eslint": "^3.0.0",
"eslint-plugin-react": "^6.0.0", "eslint-plugin-react": "^6.0.0",
@ -67,18 +75,18 @@
"karma-babel-preprocessor": "^5.2.2", "karma-babel-preprocessor": "^5.2.2",
"karma-chai": "^0.1.0", "karma-chai": "^0.1.0",
"karma-chai-sinon": "^0.1.5", "karma-chai-sinon": "^0.1.5",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.0.0", "karma-coverage": "^1.0.0",
"karma-mocha": "^1.1.1", "karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.0.4", "karma-mocha-reporter": "^2.0.4",
"karma-phantomjs-launcher": "^1.0.1", "karma-phantomjs-launcher": "^1.0.1",
"karma-sauce-launcher": "^1.0.0", "karma-sauce-launcher": "^1.1.0",
"karma-source-map-support": "^1.1.0", "karma-source-map-support": "^1.1.0",
"karma-sourcemap-loader": "^0.3.6", "karma-sourcemap-loader": "^0.3.6",
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"mocha": "^3.0.1", "mocha": "^3.0.1",
"npm-run-all": "^3.0.0", "npm-run-all": "^3.0.0",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"pretty-bytes-cli": "^2.0.0",
"rimraf": "^2.5.3", "rimraf": "^2.5.3",
"rollup": "^0.34.1", "rollup": "^0.34.1",
"rollup-plugin-babel": "^1.0.0", "rollup-plugin-babel": "^1.0.0",

View File

@ -1,4 +1,4 @@
import { ATTR_KEY, NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants'; import { NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants';
import options from '../options'; import options from '../options';
import { toLowerCase, isString, isFunction, hashToClassName } from '../util'; import { toLowerCase, isString, isFunction, hashToClassName } from '../util';
@ -20,8 +20,7 @@ export function removeNode(node) {
* @param {any} previousValue The last value that was set for this name/node pair * @param {any} previousValue The last value that was set for this name/node pair
* @private * @private
*/ */
export function setAccessor(node, name, value, old, isSvg) { export function setAccessor(node, name, old, value, isSvg) {
node[ATTR_KEY][name] = value;
if (name==='className') name = 'class'; if (name==='className') name = 'class';
@ -29,8 +28,8 @@ export function setAccessor(node, name, value, old, isSvg) {
value = hashToClassName(value); value = hashToClassName(value);
} }
if (name==='key' || name==='children' || name==='innerHTML') { if (name==='key') {
// skip these // ignore
} }
else if (name==='class' && !isSvg) { else if (name==='class' && !isSvg) {
node.className = value || ''; node.className = value || '';

View File

@ -2,8 +2,7 @@ import { VNode } from './vnode';
import options from './options'; import options from './options';
let stack = []; const stack = [];
/** JSX/hyperscript reviver /** JSX/hyperscript reviver
@ -16,7 +15,8 @@ let stack = [];
* render(<span>foo</span>, document.body); * render(<span>foo</span>, document.body);
*/ */
export function h(nodeName, attributes) { export function h(nodeName, attributes) {
let children, lastSimple, child, simple, i; let children = [],
lastSimple, child, simple, i;
for (i=arguments.length; i-- > 2; ) { for (i=arguments.length; i-- > 2; ) {
stack.push(arguments[i]); stack.push(arguments[i]);
} }
@ -35,8 +35,7 @@ export function h(nodeName, attributes) {
children[children.length-1] += child; children[children.length-1] += child;
} }
else { else {
if (children) children.push(child); children.push(child);
else children = [child];
lastSimple = simple; lastSimple = simple;
} }
} }

View File

@ -8,21 +8,17 @@ import { isString, delve } from './util';
* @private * @private
*/ */
export function createLinkedState(component, key, eventPath) { export function createLinkedState(component, key, eventPath) {
let path = key.split('.'), let path = key.split('.');
p0 = path[0];
return function(e) { return function(e) {
let t = e && e.currentTarget || this, let t = e && e.target || this,
s = component.state, state = {},
obj = s, obj = state,
v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? ((t.nodeName+t.type).match(/^input(che|rad)/i) ? t.checked : t.value) : e, v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? (t.type.match(/^che|rad/) ? t.checked : t.value) : e,
i; i = 0;
if (path.length>1) { for ( ; i<path.length-1; i++) {
for (i=0; i<path.length-1; i++) { obj = obj[path[i]] || (obj[path[i]] = !i && component.state[path[i]] || {});
obj = obj[path[i]] || (obj[path[i]] = {});
}
obj[path[i]] = v;
v = s[p0];
} }
component.setState({ [p0]: v }); obj[path[i]] = v;
component.setState(state);
}; };
} }

View File

@ -15,4 +15,13 @@ export default {
* @param {VNode} vnode A newly-created VNode to normalize/process * @param {VNode} vnode A newly-created VNode to normalize/process
*/ */
//vnode(vnode) { } //vnode(vnode) { }
/** Hook invoked after a component is mounted. */
// afterMount(component) { }
/** Hook invoked after the DOM is updated with a component's latest render. */
// afterUpdate(component) { }
/** Hook invoked immediately before a component is unmounted. */
// beforeUnmount(component) { }
}; };

19
src/preact.d.ts vendored
View File

@ -4,8 +4,14 @@ declare namespace preact {
key?:string; key?:string;
} }
interface DangerouslySetInnerHTML {
__html: string;
}
interface PreactHTMLAttributes { interface PreactHTMLAttributes {
dangerouslySetInnerHTML?:DangerouslySetInnerHTML;
key?:string; key?:string;
ref?:(el?: Element) => void;
} }
interface VNode { interface VNode {
@ -51,8 +57,8 @@ declare namespace preact {
abstract render(props:PropsType & ComponentProps, state:any):JSX.Element; abstract render(props:PropsType & ComponentProps, state:any):JSX.Element;
} }
function h<PropsType>(node:ComponentConstructor<PropsType, any>, params:PropsType, ...children:(JSX.Element|string)[]):JSX.Element; function h<PropsType>(node:ComponentConstructor<PropsType, any>, params:PropsType, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element;
function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes, ...children:(JSX.Element|string)[]):JSX.Element; function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes&{[propName: string]: any}, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element;
function render(node:JSX.Element, parent:Element, merge?:boolean):Element; function render(node:JSX.Element, parent:Element, merge?:boolean):Element;
@ -72,6 +78,11 @@ declare module "preact" {
export = preact; export = preact;
} }
declare module "preact/devtools" {
// Empty. This module initializes the React Developer Tools integration
// when imported.
}
declare namespace JSX { declare namespace JSX {
interface Element extends preact.VNode { interface Element extends preact.VNode {
@ -277,8 +288,8 @@ declare namespace JSX {
charSet?:string; charSet?:string;
challenge?:string; challenge?:string;
checked?:boolean; checked?:boolean;
class?:string; class?:string | { [key:string]: boolean };
className?:string; className?:string | { [key:string]: boolean };
cols?:number; cols?:number;
colSpan?:number; colSpan?:number;
content?:string; content?:string;

View File

@ -154,11 +154,11 @@ export function renderComponent(component, opts, mountAll, isChild) {
let baseParent = initialBase.parentNode; let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) { if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase); baseParent.replaceChild(base, initialBase);
}
if (!cbase && !toUnmount && component._parentComponent) { if (!toUnmount) {
initialBase._component = null; initialBase._component = null;
recollectNodeTree(initialBase); recollectNodeTree(initialBase);
}
} }
} }
@ -170,7 +170,9 @@ export function renderComponent(component, opts, mountAll, isChild) {
if (base && !isChild) { if (base && !isChild) {
let componentRef = component, let componentRef = component,
t = component; t = component;
while ((t=t._parentComponent)) { componentRef = t; } while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
base._component = componentRef; base._component = componentRef;
base._componentConstructor = componentRef.constructor; base._componentConstructor = componentRef.constructor;
} }
@ -179,8 +181,11 @@ export function renderComponent(component, opts, mountAll, isChild) {
if (!isUpdate || mountAll) { if (!isUpdate || mountAll) {
mounts.unshift(component); mounts.unshift(component);
} }
else if (!skip && component.componentDidUpdate) { else if (!skip) {
component.componentDidUpdate(previousProps, previousState, previousContext); if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, previousContext);
}
if (options.afterUpdate) options.afterUpdate(component);
} }
let cb = component._renderCallbacks, fn; let cb = component._renderCallbacks, fn;
@ -218,7 +223,11 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) {
} }
c = createComponent(vnode.nodeName, props, context); c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) c.nextBase = dom; if (dom && !c.nextBase) {
c.nextBase = dom;
// passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L241:
oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll); setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base; dom = c.base;
@ -239,6 +248,8 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) {
* @private * @private
*/ */
export function unmountComponent(component, remove) { export function unmountComponent(component, remove) {
if (options.beforeUnmount) options.beforeUnmount(component);
// console.log(`${remove?'Removing':'Unmounting'} component: ${component.constructor.name}`); // console.log(`${remove?'Removing':'Unmounting'} component: ${component.constructor.name}`);
let base = component.base; let base = component.base;

View File

@ -6,6 +6,7 @@ import { buildComponentFromVNode } from './component';
import { setAccessor } from '../dom/index'; import { setAccessor } from '../dom/index';
import { createNode, collectNode } from '../dom/recycler'; import { createNode, collectNode } from '../dom/recycler';
import { unmountComponent } from './component'; import { unmountComponent } from './component';
import options from '../options';
/** Diff recursion count, used to track the end of the diff cycle. */ /** Diff recursion count, used to track the end of the diff cycle. */
@ -20,6 +21,7 @@ let isSvgMode = false;
export function flushMounts() { export function flushMounts() {
let c; let c;
while ((c=mounts.pop())) { while ((c=mounts.pop())) {
if (options.afterMount) options.afterMount(c);
if (c.componentDidMount) c.componentDidMount(); if (c.componentDidMount) c.componentDidMount();
} }
} }
@ -52,7 +54,9 @@ function idiff(dom, vnode, context, mountAll) {
if (isString(vnode)) { if (isString(vnode)) {
if (dom) { if (dom) {
if (dom instanceof Text && dom.parentNode) { if (dom instanceof Text && dom.parentNode) {
dom.nodeValue = vnode; if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}
return dom; return dom;
} }
recollectNodeTree(dom); recollectNodeTree(dom);
@ -66,7 +70,8 @@ function idiff(dom, vnode, context, mountAll) {
let out = dom, let out = dom,
nodeName = vnode.nodeName, nodeName = vnode.nodeName,
prevSvgMode = isSvgMode; prevSvgMode = isSvgMode,
vchildren = vnode.children;
if (!isString(nodeName)) { if (!isString(nodeName)) {
nodeName = String(nodeName); nodeName = String(nodeName);
@ -86,11 +91,13 @@ function idiff(dom, vnode, context, mountAll) {
} }
// fast-path for elements containing a single TextNode: // fast-path for elements containing a single TextNode:
if (vnode.children && vnode.children.length===1 && typeof vnode.children[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) { if (vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) {
out.firstChild.nodeValue = vnode.children[0]; if (out.firstChild.nodeValue!=vchildren[0]) {
out.firstChild.nodeValue = vchildren[0];
}
} }
else if (vnode.children || out.firstChild) { else if (vchildren && vchildren.length || out.firstChild) {
innerDiffNode(out, vnode.children, context, mountAll); innerDiffNode(out, vchildren, context, mountAll);
} }
let props = out[ATTR_KEY]; let props = out[ATTR_KEY];
@ -232,15 +239,15 @@ export function recollectNodeTree(node, unmountOnly) {
function diffAttributes(dom, attrs, old) { function diffAttributes(dom, attrs, old) {
for (let name in old) { for (let name in old) {
if (!(attrs && name in attrs) && old[name]!=null) { if (!(attrs && name in attrs) && old[name]!=null) {
setAccessor(dom, name, null, old[name], isSvgMode); setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
} }
} }
// new & updated // new & updated
if (attrs) { if (attrs) {
for (let name in attrs) { for (let name in attrs) {
if (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name])) { if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
setAccessor(dom, name, attrs[name], old[name], isSvgMode); setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
} }
} }
} }

View File

@ -33,9 +33,10 @@ export function isNamedNode(node, nodeName) {
* @returns {Object} props * @returns {Object} props
*/ */
export function getNodeProps(vnode) { export function getNodeProps(vnode) {
let defaultProps = vnode.nodeName.defaultProps, let props = clone(vnode.attributes);
props = clone(vnode.attributes); props.children = vnode.children;
let defaultProps = vnode.nodeName.defaultProps;
if (defaultProps) { if (defaultProps) {
for (let i in defaultProps) { for (let i in defaultProps) {
if (props[i]===undefined) { if (props[i]===undefined) {
@ -44,7 +45,5 @@ export function getNodeProps(vnode) {
} }
} }
if (vnode.children) props.children = vnode.children;
return props; return props;
} }

View File

@ -70,7 +70,7 @@ describe('Components', () => {
expect(C3) expect(C3)
.to.have.been.calledOnce .to.have.been.calledOnce
.and.to.have.been.calledWith(PROPS) .and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
nodeName: 'div', nodeName: 'div',
attributes: PROPS attributes: PROPS
@ -197,7 +197,7 @@ describe('Components', () => {
expect(Outer) expect(Outer)
.to.have.been.calledOnce .to.have.been.calledOnce
.and.to.have.been.calledWith(PROPS) .and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
nodeName: Inner, nodeName: Inner,
attributes: PROPS attributes: PROPS
@ -205,7 +205,7 @@ describe('Components', () => {
expect(Inner) expect(Inner)
.to.have.been.calledOnce .to.have.been.calledOnce
.and.to.have.been.calledWith(PROPS) .and.to.have.been.calledWithMatch(PROPS)
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
nodeName: 'div', nodeName: 'div',
attributes: PROPS, attributes: PROPS,
@ -247,7 +247,7 @@ describe('Components', () => {
expect(Inner).to.have.been.calledTwice; expect(Inner).to.have.been.calledTwice;
expect(Inner.secondCall) expect(Inner.secondCall)
.to.have.been.calledWith({ foo:'bar', i:2 }) .to.have.been.calledWithMatch({ foo:'bar', i:2 })
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
attributes: { attributes: {
j: 2, j: 2,
@ -269,7 +269,7 @@ describe('Components', () => {
expect(Inner).to.have.been.calledThrice; expect(Inner).to.have.been.calledThrice;
expect(Inner.thirdCall) expect(Inner.thirdCall)
.to.have.been.calledWith({ foo:'bar', i:3 }) .to.have.been.calledWithMatch({ foo:'bar', i:3 })
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
attributes: { attributes: {
j: 3, j: 3,
@ -344,7 +344,7 @@ describe('Components', () => {
expect(Inner.prototype.render).to.have.been.calledTwice; expect(Inner.prototype.render).to.have.been.calledTwice;
expect(Inner.prototype.render.secondCall) expect(Inner.prototype.render.secondCall)
.to.have.been.calledWith({ foo:'bar', i:2 }) .to.have.been.calledWithMatch({ foo:'bar', i:2 })
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
attributes: { attributes: {
j: 2, j: 2,
@ -372,7 +372,7 @@ describe('Components', () => {
expect(Inner.prototype.render).to.have.been.calledThrice; expect(Inner.prototype.render).to.have.been.calledThrice;
expect(Inner.prototype.render.thirdCall) expect(Inner.prototype.render.thirdCall)
.to.have.been.calledWith({ foo:'bar', i:3 }) .to.have.been.calledWithMatch({ foo:'bar', i:3 })
.and.to.have.returned(sinon.match({ .and.to.have.returned(sinon.match({
attributes: { attributes: {
j: 3, j: 3,
@ -435,7 +435,7 @@ describe('Components', () => {
expect(Inner.prototype.componentDidMount).to.have.been.calledOnce; expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
expect(Inner.prototype.componentWillMount).to.have.been.calledBefore(Inner.prototype.componentDidMount); expect(Inner.prototype.componentWillMount).to.have.been.calledBefore(Inner.prototype.componentDidMount);
root = render(<asdf />, scratch, root); render(<asdf />, scratch, root);
expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce; expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce;
expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce; expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce;
@ -689,8 +689,7 @@ describe('Components', () => {
expect(C1.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C1').not.to.have.been.called; 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; 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(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; expect(C3.prototype.componentDidUnmount, 'unmount innermost w/ intermediary div, C3').to.have.been.calledOnce;
reset(); reset();

View File

@ -1,6 +1,8 @@
import { h, render, Component } from '../../src/preact'; import { h, render, Component } from '../../src/preact';
/** @jsx h */ /** @jsx h */
const CHILDREN_MATCHER = sinon.match( v => v==null || Array.isArray(v) && !v.length , '[empty children]');
describe('context', () => { describe('context', () => {
let scratch; let scratch;
@ -57,18 +59,19 @@ describe('context', () => {
expect(Outer.prototype.getChildContext).to.have.been.calledOnce; expect(Outer.prototype.getChildContext).to.have.been.calledOnce;
// initial render does not invoke anything but render(): // initial render does not invoke anything but render():
expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); expect(Inner.prototype.render).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}, CONTEXT);
CONTEXT.foo = 'bar'; CONTEXT.foo = 'bar';
render(<Outer {...PROPS} />, scratch, scratch.lastChild); render(<Outer {...PROPS} />, scratch, scratch.lastChild);
expect(Outer.prototype.getChildContext).to.have.been.calledTwice; expect(Outer.prototype.getChildContext).to.have.been.calledTwice;
expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); let props = { children: CHILDREN_MATCHER, ...PROPS };
expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT);
expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT);
expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {});
expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {});
expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT);
/* Future: /* Future:
@ -115,18 +118,19 @@ describe('context', () => {
expect(Outer.prototype.getChildContext).to.have.been.calledOnce; expect(Outer.prototype.getChildContext).to.have.been.calledOnce;
// initial render does not invoke anything but render(): // initial render does not invoke anything but render():
expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, CONTEXT);
CONTEXT.foo = 'bar'; CONTEXT.foo = 'bar';
render(<Outer {...PROPS} />, scratch, scratch.lastChild); render(<Outer {...PROPS} />, scratch, scratch.lastChild);
expect(Outer.prototype.getChildContext).to.have.been.calledTwice; expect(Outer.prototype.getChildContext).to.have.been.calledTwice;
expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); let props = { children: CHILDREN_MATCHER, ...PROPS };
expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT);
expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT);
expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {});
expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {});
expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT);
// make sure render() could make use of context.a // make sure render() could make use of context.a
expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] })); expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] }));
@ -164,7 +168,7 @@ describe('context', () => {
render(<Outer />, scratch); render(<Outer />, scratch);
expect(Inner.prototype.render).to.have.been.calledWith({}, {}, { outerContext }); expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext });
expect(InnerMost.prototype.render).to.have.been.calledWith({}, {}, { outerContext, innerContext }); expect(InnerMost.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext, innerContext });
}); });
}); });

234
test/browser/devtools.js Normal file
View File

@ -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);
});
});

View File

@ -3,6 +3,8 @@ import { h, render, rerender, Component } from '../../src/preact';
let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) ); let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) );
const EMPTY_CHILDREN = [];
describe('Lifecycle methods', () => { describe('Lifecycle methods', () => {
let scratch; let scratch;
@ -50,7 +52,7 @@ describe('Lifecycle methods', () => {
} }
class Inner extends Component { class Inner extends Component {
componentWillUpdate(nextProps, nextState) { componentWillUpdate(nextProps, nextState) {
expect(nextProps).to.be.deep.equal({i: 1}); expect(nextProps).to.be.deep.equal({ children:EMPTY_CHILDREN, i: 1 });
expect(nextState).to.be.deep.equal({}); expect(nextState).to.be.deep.equal({});
} }
render() { render() {

View File

@ -26,7 +26,10 @@ describe('linked-state', () => {
element.type= 'text'; element.type= 'text';
element.value = 'newValue'; element.value = 'newValue';
linkFunction({ currentTarget: element }); linkFunction({
currentTarget: element,
target: element
});
expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledOnce;
expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'newValue'}); expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'newValue'});
@ -42,7 +45,10 @@ describe('linked-state', () => {
checkboxElement.type= 'checkbox'; checkboxElement.type= 'checkbox';
checkboxElement.checked = true; checkboxElement.checked = true;
linkFunction({ currentTarget: checkboxElement }); linkFunction({
currentTarget: checkboxElement,
target: checkboxElement
});
expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledOnce;
expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true});
@ -53,7 +59,10 @@ describe('linked-state', () => {
radioElement.type= 'radio'; radioElement.type= 'radio';
radioElement.checked = true; radioElement.checked = true;
linkFunction({ currentTarget: radioElement }); linkFunction({
currentTarget: radioElement,
target: radioElement
});
expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledOnce;
expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true});
@ -66,7 +75,10 @@ describe('linked-state', () => {
element.type= 'text'; element.type= 'text';
element.value = 'newValue'; element.value = 'newValue';
linkFunction({ currentTarget: element }); linkFunction({
currentTarget: element,
target: element
});
expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledOnce;
expect(TestComponent.prototype.setState).to.have.been.calledWith({nested: {state: {key: 'newValue'}}}); expect(TestComponent.prototype.setState).to.have.been.calledWith({nested: {state: {key: 'newValue'}}});

View File

@ -200,8 +200,8 @@ describe('refs', () => {
</div> </div>
), scratch); ), scratch);
expect(Foo.prototype.render).to.have.been.calledWithExactly({ a:'a' }, { }, { }); expect(Foo.prototype.render).to.have.been.calledWithMatch({ ref:sinon.match.falsy, a:'a' }, { }, { });
expect(Bar).to.have.been.calledWithExactly({ b:'b', ref:bar }, { }); expect(Bar).to.have.been.calledWithMatch({ b:'b', ref:bar }, { });
}); });
// Test for #232 // Test for #232
@ -284,4 +284,22 @@ describe('refs', () => {
expect(inst.handleMount.firstCall).to.have.been.calledWith(null); expect(inst.handleMount.firstCall).to.have.been.calledWith(null);
expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#div')); expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#div'));
}); });
it('should add refs to components representing DOM nodes with no attributes if they have been pre-rendered', () => {
// Simulate pre-render
let parent = document.createElement('div');
let child = document.createElement('div');
parent.appendChild(child);
scratch.appendChild(parent); // scratch contains: <div><div></div></div>
let ref = spy('ref');
function Wrapper() {
return <div></div>;
}
render(<div><Wrapper ref={ref} /></div>, scratch, scratch.firstChild);
expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild.firstChild);
});
}); });

View File

@ -1,6 +1,8 @@
import { h, render, rerender, Component } from '../../src/preact'; import { h, render, rerender, Component } from '../../src/preact';
/** @jsx h */ /** @jsx h */
const EMPTY_CHILDREN = [];
describe('Component spec', () => { describe('Component spec', () => {
let scratch; let scratch;
@ -24,6 +26,7 @@ describe('Component spec', () => {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
expect(props).to.be.deep.equal({ expect(props).to.be.deep.equal({
children: EMPTY_CHILDREN,
fieldA: 1, fieldB: 2, fieldA: 1, fieldB: 2,
fieldC: 1, fieldD: 2 fieldC: 1, fieldD: 2
}); });
@ -81,14 +84,14 @@ describe('Component spec', () => {
fieldC: 1, fieldD: 2 fieldC: 1, fieldD: 2
}; };
expect(proto.ctor).to.have.been.calledWith(PROPS1); expect(proto.ctor).to.have.been.calledWithMatch(PROPS1);
expect(proto.render).to.have.been.calledWith(PROPS1); expect(proto.render).to.have.been.calledWithMatch(PROPS1);
rerender(); rerender();
// expect(proto.ctor).to.have.been.calledWith(PROPS2); // expect(proto.ctor).to.have.been.calledWith(PROPS2);
expect(proto.componentWillReceiveProps).to.have.been.calledWith(PROPS2); expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch(PROPS2);
expect(proto.render).to.have.been.calledWith(PROPS2); expect(proto.render).to.have.been.calledWithMatch(PROPS2);
}); });
// @TODO: migrate this to preact-compat // @TODO: migrate this to preact-compat

View File

@ -1,45 +1,66 @@
/*eslint no-var:0, object-shorthand:0 */ /*eslint no-var:0, object-shorthand:0 */
var coverage = String(process.env.COVERAGE)!=='false', var coverage = String(process.env.COVERAGE)!=='false',
sauceLabs = String(process.env.SAUCELABS).match(/^(1|true)$/gi) && !String(process.env.TRAVIS_PULL_REQUEST).match(/^(1|true)$/gi), ci = String(process.env.CI).match(/^(1|true)$/gi),
performance = !coverage && !sauceLabs && String(process.env.PERFORMANCE)!=='false', pullRequest = !String(process.env.TRAVIS_PULL_REQUEST).match(/^(0|false|undefined)$/gi),
realBrowser = String(process.env.BROWSER).match(/^(1|true)$/gi),
sauceLabs = realBrowser && ci && !pullRequest,
performance = !coverage && !realBrowser && String(process.env.PERFORMANCE)!=='false',
webpack = require('webpack'); webpack = require('webpack');
var sauceLabsLaunchers = { var sauceLabsLaunchers = {
sl_chrome: { sl_chrome: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'chrome' browserName: 'chrome',
platform: 'Windows 10'
}, },
sl_firefox: { sl_firefox: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'firefox' browserName: 'firefox',
platform: 'Windows 10'
}, },
sl_ios_safari: { sl_safari: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'iphone', browserName: 'safari',
platform: 'OS X 10.9', platform: 'OS X 10.11'
version: '7.1' },
sl_edge: {
base: 'SauceLabs',
browserName: 'MicrosoftEdge',
platform: 'Windows 10'
}, },
sl_ie_11: { sl_ie_11: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'internet explorer', browserName: 'internet explorer',
version: '11' version: '11.103',
platform: 'Windows 10'
}, },
sl_ie_10: { sl_ie_10: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'internet explorer', browserName: 'internet explorer',
version: '10' version: '10.0',
platform: 'Windows 7'
}, },
sl_ie_9: { sl_ie_9: {
base: 'SauceLabs', base: 'SauceLabs',
browserName: 'internet explorer', browserName: 'internet explorer',
version: '9' version: '9.0',
platform: 'Windows 7'
} }
}; };
var travisLaunchers = {
chrome_travis: {
base: 'Chrome',
flags: ['--no-sandbox']
}
};
var localBrowsers = realBrowser ? Object.keys(travisLaunchers) : ['PhantomJS'];
module.exports = function(config) { module.exports = function(config) {
config.set({ config.set({
browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : ['PhantomJS'], browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : localBrowsers,
frameworks: ['source-map-support', 'mocha', 'chai-sinon'], frameworks: ['source-map-support', 'mocha', 'chai-sinon'],
@ -69,14 +90,18 @@ module.exports = function(config) {
browserNoActivityTimeout: 5 * 60 * 1000, browserNoActivityTimeout: 5 * 60 * 1000,
// Use only two browsers concurrently, works better with open source Sauce Labs remote testing
concurrency: 2,
// sauceLabs: { // sauceLabs: {
// tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER || ('local'+require('./package.json').version), // tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER || ('local'+require('./package.json').version),
// startConnect: false // startConnect: false
// }, // },
customLaunchers: sauceLabsLaunchers, customLaunchers: sauceLabs ? sauceLabsLaunchers : travisLaunchers,
files: [ files: [
{ pattern: 'polyfills.js', watched: false },
{ pattern: '{browser,shared}/**.js', watched: false } { pattern: '{browser,shared}/**.js', watched: false }
], ],
@ -107,6 +132,10 @@ module.exports = function(config) {
} : []) } : [])
}, },
resolve: { resolve: {
// The React DevTools integration requires preact as a module
// rather than referencing source files inside the module
// directly
alias: { preact: '../src/preact' },
modulesDirectories: [__dirname, 'node_modules'] modulesDirectories: [__dirname, 'node_modules']
}, },
plugins: [ plugins: [

5
test/polyfills.js Normal file
View File

@ -0,0 +1,5 @@
// ES2015 APIs used by developer tools integration
import 'core-js/es6/map';
import 'core-js/fn/array/fill';
import 'core-js/fn/array/from';
import 'core-js/fn/object/assign';

View File

@ -6,7 +6,12 @@ import { expect } from 'chai';
/** @jsx h */ /** @jsx h */
let flatten = obj => JSON.parse(JSON.stringify(obj)); const buildVNode = (nodeName, attributes, children=[]) => ({
nodeName,
children,
attributes,
key: attributes && attributes.key
});
describe('h(jsx)', () => { describe('h(jsx)', () => {
it('should return a VNode', () => { it('should return a VNode', () => {
@ -16,7 +21,7 @@ describe('h(jsx)', () => {
expect(r).to.be.an.instanceof(VNode); expect(r).to.be.an.instanceof(VNode);
expect(r).to.have.property('nodeName', 'foo'); expect(r).to.have.property('nodeName', 'foo');
expect(r).to.have.property('attributes', undefined); expect(r).to.have.property('attributes', undefined);
expect(r).to.have.property('children', undefined); expect(r).to.have.property('children').that.eql([]);
}); });
it('should perserve raw attributes', () => { it('should perserve raw attributes', () => {
@ -38,8 +43,8 @@ describe('h(jsx)', () => {
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([
new VNode('bar'), buildVNode('bar'),
new VNode('baz') buildVNode('baz')
]); ]);
}); });
@ -51,15 +56,13 @@ describe('h(jsx)', () => {
h('baz', null, h('test')) h('baz', null, h('test'))
); );
r = flatten(r);
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([
{ nodeName:'bar' }, buildVNode('bar'),
{ nodeName:'baz', children:[ buildVNode('baz', undefined, [
{ nodeName:'test' } buildVNode('test')
]} ])
]); ]);
}); });
@ -73,15 +76,13 @@ describe('h(jsx)', () => {
] ]
); );
r = flatten(r);
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([
{ nodeName:'bar' }, buildVNode('bar'),
{ nodeName:'baz', children:[ buildVNode('baz', undefined, [
{ nodeName:'test' } buildVNode('test')
]} ])
]); ]);
}); });
@ -95,15 +96,13 @@ describe('h(jsx)', () => {
] ]
); );
r = flatten(r);
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([
{ nodeName:'bar' }, buildVNode('bar'),
{ nodeName:'baz', children:[ buildVNode('baz', undefined, [
{ nodeName:'test' } buildVNode('test')
]} ])
]); ]);
}); });
@ -164,16 +163,14 @@ describe('h(jsx)', () => {
'six' 'six'
); );
r = flatten(r);
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([
'onetwo', 'onetwo',
{ nodeName:'bar' }, buildVNode('bar'),
'three', 'three',
{ nodeName:'baz' }, buildVNode('baz'),
{ nodeName:'baz' }, buildVNode('baz'),
'fourfivesix' 'fourfivesix'
]); ]);
}); });
@ -190,8 +187,6 @@ describe('h(jsx)', () => {
null null
); );
r = flatten(r);
expect(r).to.be.an('object') expect(r).to.be.an('object')
.with.property('children') .with.property('children')
.that.deep.equals([ .that.deep.equals([