diff --git a/.eslintrc b/.eslintrc index 09c3532..2b12303 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,7 +17,8 @@ "mocha": true, "node": true, "phantomjs": true, - "worker": true + "worker": true, + "jest": true }, "globals": { "__DEV__": true diff --git a/README.md b/README.md index 71e275f..d5d0ba2 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,41 @@ If you need an onScroll handler, just add the handler to the div wrapping your R ``` +##### Why are my unit tests failing with `TypeError: Cannot read property 'parentElement' of null`? + +Your're probably using [react-test-render](https://github.com/facebook/react/tree/master/packages/react-test-renderer) to create snapshot tests. `react-test-render` is an +abstraction layer and knows nothing about the react specific `ref` feature to get DOM nodes. +Therefore the `null` element access. + +However, you can pass options to the `react-test-renderer`: + +```js +import ReactTestRenderer from 'react-test-renderer'; + +function render (component) { + const reactTestRendererOptions = { + createNodeMock + }; + return ReactTestRenderer.create(component, reactTestRendererOptions); +} + +function isDOMElementType(type) { + return [ + 'div', + 'ul', + 'table', + // ... + ].includes(type); +} + +function createNodeMock(element) { + if (isDOMElementType(element.type)) { + return document.createElement(element.type); + } + return null; +} +``` + ## Development ```bash diff --git a/__tests__/__snapshots__/react-list.spec.js.snap b/__tests__/__snapshots__/react-list.spec.js.snap new file mode 100644 index 0000000..881f061 --- /dev/null +++ b/__tests__/__snapshots__/react-list.spec.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react-list uniform react-list renders into DOM 1`] = ` +
+
+ +
+
+`; + +exports[`react-list uniform react-list renders with react-test-renderer 1`] = ` +
+
+ +
+
+`; diff --git a/__tests__/react-list.spec.js b/__tests__/react-list.spec.js new file mode 100644 index 0000000..bf2966e --- /dev/null +++ b/__tests__/react-list.spec.js @@ -0,0 +1,82 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import ReactTestRenderer from 'react-test-renderer'; +import ReactList from './../react-list'; + +describe('react-list', function () { + + const createNodeMock = function createNodeMock(element) { + if (element.type === 'div') { + // div container of react-list + return document.createElement('div'); + } + if (element.type === 'ul') { + // element of the + return document.createElement('ul'); + } + // You can return any object from this method for any type of DOM component. + // React will use it as a ref instead of a DOM node when snapshot testing. + return null; + }; + + const render = function render(component) { + const options = { + createNodeMock + }; + return ReactTestRenderer.create(component, options); + }; + + class MyList extends React.Component { + constructor(props) { + super(props); + this.listRenderer = this.listRenderer.bind(this); + this.listItemRenderer = this.listItemRenderer.bind(this); + } + + listItemRenderer(index, key) { + return ( +
  • + list item: {this.props.items[index]} +
  • + ); + } + + listRenderer(items, ref) { + return ( + + ); + } + + render() { + return ( + + ); + } + } + + describe('uniform react-list', () => { + + it('renders with react-test-renderer', function () { + const tree = render( + + ); + expect(tree).toMatchSnapshot(); + }); + + it('renders into DOM', function () { + const tree = ReactTestUtils.renderIntoDocument( + + ); + const domNode = ReactDOM.findDOMNode(tree); + expect(domNode).toMatchSnapshot(); + }); + }); +}); diff --git a/package.json b/package.json index e8f4364..b801186 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,11 @@ "type": "git", "url": "https://github.com/orgsync/react-list" }, + "scripts": { + "prebuild": "npm run test", + "build": "cogs", + "test": "NODE_ENV=test jest" + }, "dependencies": { "prop-types": "15" }, @@ -26,6 +31,20 @@ "cogs-transformer-eslint": "^3.0.0", "cogs-transformer-replace": "^3.0.0", "eslint": "^3.4.0", - "eslint-plugin-react": "^6.2.0" + "eslint-plugin-react": "^6.2.0", + "jest": "^20.0.4", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-test-renderer": "^15.6.1" + }, + "babel": { + "env": { + "test": { + "presets": [ + "es2015", + "react" + ] + } + } } } diff --git a/react-list.es6 b/react-list.es6 index 3af2a81..f0a878a 100644 --- a/react-list.es6 +++ b/react-list.es6 @@ -1,9 +1,6 @@ import module from 'module'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import ReactDOM from 'react-dom'; - -const {findDOMNode} = ReactDOM; const CLIENT_SIZE_KEYS = {x: 'clientWidth', y: 'clientHeight'}; const CLIENT_START_KEYS = {x: 'clientTop', y: 'clientLeft'}; @@ -142,7 +139,7 @@ module.exports = class ReactList extends Component { getScrollParent() { const {axis, scrollParentGetter} = this.props; if (scrollParentGetter) return scrollParentGetter(); - let el = findDOMNode(this); + let el = this.rootDOMNode; const overflowKey = OVERFLOW_KEYS[axis]; while (el = el.parentElement) { switch (window.getComputedStyle(el)[overflowKey]) { @@ -164,14 +161,14 @@ module.exports = class ReactList extends Component { scrollParent[scrollKey]; const max = this.getScrollSize() - this.getViewportSize(); const scroll = Math.max(0, Math.min(actual, max)); - const el = findDOMNode(this); + const el = this.rootDOMNode; return this.getOffset(scrollParent) + scroll - this.getOffset(el); } setScroll(offset) { const {scrollParent} = this; const {axis} = this.props; - offset += this.getOffset(findDOMNode(this)); + offset += this.getOffset(this.rootDOMNode); if (scrollParent === window) return window.scrollTo(0, offset); offset -= this.getOffset(this.scrollParent); @@ -217,7 +214,7 @@ module.exports = class ReactList extends Component { return {itemSize, itemsPerRow}; } - const itemEls = findDOMNode(this.items).children; + const itemEls = this.items.children; if (!itemEls.length) return {}; const firstEl = itemEls[0]; @@ -268,7 +265,7 @@ module.exports = class ReactList extends Component { updateSimpleFrame(cb) { const {end} = this.getStartAndEnd(); - const itemEls = findDOMNode(this.items).children; + const itemEls = this.items.children; let elEnd = 0; if (itemEls.length) { @@ -363,7 +360,7 @@ module.exports = class ReactList extends Component { cacheSizes() { const {cache} = this; const {from} = this.state; - const itemEls = findDOMNode(this.items).children; + const itemEls = this.items.children; const sizeKey = OFFSET_SIZE_KEYS[this.props.axis]; for (let i = 0, l = itemEls.length; i < l; ++i) { cache[from + i] = itemEls[i][sizeKey]; @@ -386,7 +383,7 @@ module.exports = class ReactList extends Component { // Try the DOM. if (type === 'simple' && index >= from && index < from + size && items) { - const itemEl = findDOMNode(items).children[index - from]; + const itemEl = items.children[index - from]; if (itemEl) return itemEl[OFFSET_SIZE_KEYS[axis]]; } @@ -474,6 +471,6 @@ module.exports = class ReactList extends Component { WebkitTransform: transform, transform }; - return
    {items}
    ; + return
    this.rootDOMNode = node}>
    {items}
    ; } }; diff --git a/react-list.js b/react-list.js index 5656d08..68a50f4 100644 --- a/react-list.js +++ b/react-list.js @@ -1,16 +1,16 @@ (function (global, factory) { if (typeof define === "function" && define.amd) { - define(['module', 'prop-types', 'react', 'react-dom'], factory); + define(['module', 'prop-types', 'react'], factory); } else if (typeof exports !== "undefined") { - factory(module, require('prop-types'), require('react'), require('react-dom')); + factory(module, require('prop-types'), require('react')); } else { var mod = { exports: {} }; - factory(mod, global.PropTypes, global.React, global.ReactDOM); + factory(mod, global.PropTypes, global.React); global.ReactList = mod.exports; } -})(this, function (_module2, _propTypes, _react, _reactDom) { +})(this, function (_module2, _propTypes, _react) { 'use strict'; var _module3 = _interopRequireDefault(_module2); @@ -19,14 +19,26 @@ var _react2 = _interopRequireDefault(_react); - var _reactDom2 = _interopRequireDefault(_reactDom); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -77,9 +89,6 @@ var _class, _temp; - var findDOMNode = _reactDom2.default.findDOMNode; - - var CLIENT_SIZE_KEYS = { x: 'clientWidth', y: 'clientHeight' }; var CLIENT_START_KEYS = { x: 'clientTop', y: 'clientLeft' }; var INNER_SIZE_KEYS = { x: 'innerWidth', y: 'innerHeight' }; @@ -215,7 +224,7 @@ scrollParentGetter = _props.scrollParentGetter; if (scrollParentGetter) return scrollParentGetter(); - var el = findDOMNode(this); + var el = this.rootDOMNode; var overflowKey = OVERFLOW_KEYS[axis]; while (el = el.parentElement) { switch (window.getComputedStyle(el)[overflowKey]) { @@ -239,7 +248,7 @@ document.body[scrollKey] || document.documentElement[scrollKey] : scrollParent[scrollKey]; var max = this.getScrollSize() - this.getViewportSize(); var scroll = Math.max(0, Math.min(actual, max)); - var el = findDOMNode(this); + var el = this.rootDOMNode; return this.getOffset(scrollParent) + scroll - this.getOffset(el); } }, { @@ -248,7 +257,7 @@ var scrollParent = this.scrollParent; var axis = this.props.axis; - offset += this.getOffset(findDOMNode(this)); + offset += this.getOffset(this.rootDOMNode); if (scrollParent === window) return window.scrollTo(0, offset); offset -= this.getOffset(this.scrollParent); @@ -309,7 +318,7 @@ return { itemSize: itemSize, itemsPerRow: itemsPerRow }; } - var itemEls = findDOMNode(this.items).children; + var itemEls = this.items.children; if (!itemEls.length) return {}; var firstEl = itemEls[0]; @@ -364,7 +373,7 @@ var _getStartAndEnd = this.getStartAndEnd(), end = _getStartAndEnd.end; - var itemEls = findDOMNode(this.items).children; + var itemEls = this.items.children; var elEnd = 0; if (itemEls.length) { @@ -479,7 +488,7 @@ var cache = this.cache; var from = this.state.from; - var itemEls = findDOMNode(this.items).children; + var itemEls = this.items.children; var sizeKey = OFFSET_SIZE_KEYS[this.props.axis]; for (var i = 0, l = itemEls.length; i < l; ++i) { cache[from + i] = itemEls[i][sizeKey]; @@ -512,7 +521,7 @@ // Try the DOM. if (type === 'simple' && index >= from && index < from + size && items) { - var itemEl = findDOMNode(items).children[index - from]; + var itemEl = items.children[index - from]; if (itemEl) return itemEl[OFFSET_SIZE_KEYS[axis]]; } @@ -599,6 +608,8 @@ }, { key: 'render', value: function render() { + var _this4 = this; + var _props8 = this.props, axis = _props8.axis, length = _props8.length, @@ -631,7 +642,9 @@ }; return _react2.default.createElement( 'div', - { style: style }, + _extends({ style: style }, { ref: function ref(node) { + return _this4.rootDOMNode = node; + } }), _react2.default.createElement( 'div', { style: listStyle },