UNPKG

omniscient

Version:

A library providing an abstraction for React components for passing the same data structure through the entire component flow using cursors and immutable data structures.

822 lines (641 loc) 23.3 kB
var Immutable = require('immutable'); var Cursor = require('immutable/contrib/cursor'); var createClass = require('create-react-class'); var React = require('react'); var ReactDOM = require('react-dom'); var component = require('../'); var shouldUpdateMixin = require('../shouldupdate'); describe('component', () => { describe('arguments', () => { test('should take displayName', done => { var mixins = [{ componentDidMount: () => done() }]; var Component = component('myComponent', mixins, function() { expect(this.constructor).toHaveProperty('displayName'); expect(this.constructor.displayName).toBe('myComponent'); return textNode('hello'); }); render(Component()); }); test('should set displayName from render function name', done => { var mixins = [{ componentDidMount: () => done() }]; var Component = component(mixins, function MyComponentName() { expect(this.constructor).toHaveProperty('displayName'); expect(this.constructor.displayName).toBe('MyComponentName'); return textNode('hello'); }); render(Component()); }); }); describe('decorator', () => { test('should attach decorator', () => { var hasBeenCalled = false; var decorator = function(SomeClass) { hasBeenCalled = true; return SomeClass; }; var decoratedComponent = component.withDefaults({ classDecorator: decorator }); var Component = decoratedComponent(function MyComponentName() { return textNode('hello'); }); render(Component()); expect(hasBeenCalled).toBe(true); }); test('should attach inline decorator', () => { var hasBeenCalled = false; var decorator = function(SomeClass) { hasBeenCalled = true; return SomeClass; }; var Component = component.classDecorator( decorator, function MyComponentName() { return textNode('hello'); } ); render(Component()); expect(hasBeenCalled).toBe(true); }); test('should allow for creating a partially applied classDecorator factory', () => { var hasBeenCalled = false; var decorator = function(SomeClass) { hasBeenCalled = true; return SomeClass; }; var Component = component.classDecorator(decorator)( { foo: function() {} }, function MyComponentName() { expect(typeof this.foo).toBe('function'); return textNode('hello'); } ); render(Component()); expect(hasBeenCalled).toBe(true); }); test('should allow to extend class as decorator', () => { var decorator = function(ComposedComponent) { expect(ComposedComponent.displayName).toBe('MyComponentName'); ComposedComponent.displayName = 'Foobar'; return ComposedComponent; }; var decoratedComponent = component.withDefaults({ classDecorator: decorator }); var Component = decoratedComponent(function MyComponentName() { expect(this.constructor.displayName).toBe('Foobar'); return textNode('hello'); }); render(Component()); }); }); describe('statics', () => { test('should take static methods', () => { var mixins = [{ statics: { foo: noop, bar: noop } }]; var Component = component(mixins, function() { return textNode('hello'); }); expect(typeof Component.foo).toBe('function'); expect(typeof Component.bar).toBe('function'); }); test('should take static methods from several mixins', () => { var mixins = [{ statics: { foo: noop } }, { statics: { bar: noop } }]; var Component = component(mixins, function() { return textNode('hello'); }); expect(typeof Component.foo).toBe('function'); expect(typeof Component.bar).toBe('function'); }); }); describe('mixins', () => { test('should take mixins', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var Component = component(mixins, function() { expect(this).toHaveProperty('myMixin'); return textNode('hello'); }); render(Component()); }); test('should take single object as mixin', done => { var mixins = { componentDidMount: () => done(), myMixin: noop }; var Component = component(mixins, function() { expect(this).toHaveProperty('myMixin'); return textNode('hello'); }); render(Component()); }); test('should have overridable shouldComponentUpdate in mixin', done => { var shouldUpdate = function(nextProps) { return true; }; var mixins = [ { componentDidMount: () => done(), shouldComponentUpdate: shouldUpdate } ]; var Component = component(mixins, function() { expect(this.shouldComponentUpdate).toBe(shouldUpdate); return textNode('hello'); }); render(Component({ foo: 'hello' })); }); test('should have overridable shouldComponentUpdate in nested mixin', done => { var shouldUpdate = function(nextProps) { return true; }; var mixins = { componentDidMount: () => done(), mixins: [ { mixins: [{ shouldComponentUpdate: shouldUpdate }] } ] }; var Component = component(mixins, function() { expect(this.shouldComponentUpdate).toBe(shouldUpdate); return textNode('hello'); }); render(Component({ foo: 'hello' })); }); test('should allow shouldComponentUpdate as mixin to vanilla React', done => { var mixins = [ { shouldComponentUpdate: shouldUpdateMixin } ]; var Component = createClass({ mixins: mixins, render: function() { expect(this.shouldComponentUpdate).toBe(shouldUpdateMixin); done(); return textNode('hello'); } }); render(React.createElement(Component, { foo: 'hello' })); }); }); describe('render function arguments', () => { test('should handle no arguments', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var Component = component(mixins, function(cursor, statics) { expect(cursor).toEqual({}); expect(statics).toBeFalsy(); return textNode('hello'); }); render(Component()); }); test('should pass single cursor', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var cursor1 = { cursor: {} }; var Component = component(mixins, function(cursor) { expect(cursor).toEqual(cursor1); return textNode('hello'); }); render(Component(cursor1)); }); test('should pass objected cursor', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var cursor1 = { cursor: {} }; var Component = component(mixins, function(props) { expect(props.cursor).toEqual(cursor1); return textNode('hello'); }); render(Component({ cursor: cursor1 })); }); test('should pass and expose single immutable cursor', done => { var mixins = [{ componentDidMount: () => done() }]; var cursorInput = Cursor.from(Immutable.fromJS({ foo: 'hello' }), 'foo'); var Component = component(mixins, function(cursor) { expect(cursor).toBe(cursorInput); return textNode('hello'); }); render(Component(cursorInput)); }); test('should pass and expose single immutable cursor on this.props', done => { var mixins = [{ componentDidMount: () => done() }]; var cursorInput = Cursor.from(Immutable.fromJS({ foo: 'hello' }), 'foo'); var Component = component(mixins, function(cursor) { expect(this.cursor).toBe(cursor); expect(this.cursor).toBe(cursorInput); return textNode('hello'); }); render(Component(cursorInput)); }); test('should pass and expose single immutable cursor on this.props and re-render', () => { var cursorInput = Cursor.from(Immutable.fromJS({ foo: 'hello' }), 'foo'); var i = 0; var Component = component(function(cursor) { i++; return textNode('hello'); }); render(Component(cursorInput)); render(Component(cursorInput)); render( Component( cursorInput.update(function() { return 'bar'; }) ) ); expect(i).toBe(2); }); test('should pass single cursor', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var input = { cursor: 'foo' }; var Component = component(mixins, function(cursor) { expect(cursor).toEqual(input); return textNode('hello'); }); render(Component(input)); }); test('should pass single immutable structure', done => { var mixins = [{ componentDidMount: () => done(), myMixin: noop }]; var imm = Immutable.List.of(1); var Component = component(mixins, function(immutableStructure) { expect(immutableStructure).toEqual(imm); return textNode('hello'); }); render(Component(imm)); }); test('should set React key', done => { var mixins = [{ componentDidMount: () => done() }]; var Component = component(mixins, function() { hasKey(this, 'myKey'); return textNode('hello'); }); render(Component('myKey')); }); test('should get passed key and cursor-objects', done => { var mixins = [{ componentDidMount: () => done() }]; var Component = component(mixins, function(data) { hasKey(this, 'myKey'); expect(data).toHaveProperty('foo'); expect(data.foo).toBe('hello'); return textNode('hello'); }); render(Component('myKey', { foo: 'hello' })); }); test('should not mutate the props passed', done => { var mixins = [{ componentDidMount: () => done() }]; var props = { foo: 'hello' }; var Component = component(mixins, function(data) { expect(data).toHaveProperty('foo'); expect(data.foo).toBe('hello'); return textNode('hello'); }); render(Component(props)); }); test('should get passed key and immutable cursor-objects', done => { var mixins = [{ componentDidMount: () => done() }]; var cursorInput = Cursor.from(Immutable.fromJS({ foo: 'hello' }), 'foo'); var Component = component(mixins, function(cursor) { hasKey(this, 'myKey'); expect(cursor).toBe(cursorInput); return textNode('hello'); }); render(Component('myKey', cursorInput)); }); test('should get passed key, cursor-objects and statics', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { cursor: 'hello' }; var Component = component(mixins, function(props) { hasKey(this, 'myKey'); expect(props.cursor).toBe(outerCursor.cursor); return textNode('hello'); }); render(Component('myKey', outerCursor)); }); test('should get passed cursor-object and children', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { foo: 'hello' }; var c1 = textNode('hello'); var c2 = textNode('bar'); var Component = component(mixins, function(cursor) { expect(cursor.foo).toBe(cursor.foo); expect(this.props.children).toHaveLength(2); expect(this.props.children[0]).toBe(c1); expect(this.props.children[1]).toBe(c2); return textNode('hello'); }); render(Component(outerCursor, c1, c2)); }); test('should get passed key, cursor-object and children', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { foo: 'hello' }; var Component = component(mixins, function(cursor) { hasKey(this, 'myKey'); expect(cursor.foo).toBe(outerCursor.foo); expect(this.props.children).toHaveLength(1); return textNode('hello'); }); render(Component('myKey', outerCursor, textNode('hello'))); }); test('should get passed cursor-object and children', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { foo: 'hello' }; var Component = component(mixins, function(cursor, statics) { expect(cursor.foo).toBe(outerCursor.foo); expect(this.props.children).toHaveLength(1); return textNode('hello'); }); render(Component(outerCursor, textNode('hello'))); }); test('should pass multiple cursors', done => { var mixins = [{ componentDidMount: () => done() }]; var cursor1 = {}; var cursor2 = {}; var Component = component(mixins, function(cursor, staticarg) { expect(cursor.one).toBe(cursor1); expect(cursor.two).toBe(cursor2); return textNode('hello'); }); render(Component({ one: cursor1, two: cursor2 })); }); test('can take strings as children', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { foo: 'hello' }; var c1 = 'hello'; var c2 = 'world'; var Component = component(mixins, function(cursor) { expect(cursor.foo).toBe(cursor.foo); expect(this.props.children).toHaveLength(2); expect(this.props.children[0]).toBe(c1); expect(this.props.children[1]).toBe(c2); return textNode(this.props.children); }); render(Component(outerCursor, c1, c2)); }); test('can take arrays as children', done => { var mixins = [{ componentDidMount: () => done() }]; var outerCursor = { foo: 'hello' }; var c1 = ['hello', 'world']; var Component = component(mixins, function(cursor) { expect(cursor.foo).toBe(cursor.foo); expect(this.props.children).toHaveLength(2); expect(this.props.children[0]).toBe(c1[0]); expect(this.props.children[1]).toBe(c1[1]); return textNode(this.props.children); }); render(Component(outerCursor, c1)); }); test('can take props & children', done => { var mixins = [{ componentDidMount: () => done() }]; var c1 = ['hello', 'world']; var Component = component(mixins, function(cursor) { expect(this.props.children).toHaveLength(2); expect(this.props.children[0]).toBe(c1[0]); expect(this.props.children[1]).toBe(c1[1]); return textNode(this.props.children); }); render(Component({}, c1)); }); test('can take key, props & children', done => { var mixins = [{ componentDidMount: () => done() }]; var c1 = ['hello', 'world']; var Component = component(mixins, function(cursor) { hasKey(this, 'myKey'); expect(this.props.children).toHaveLength(2); expect(this.props.children[0]).toBe(c1[0]); expect(this.props.children[1]).toBe(c1[1]); return textNode(this.props.children); }); render(Component('myKey', {}, c1)); }); }); describe('overridables', () => { test('should have overridable shouldComponentUpdate', done => { var shouldUpdate = function() { return true; }; var localComponent = component.withDefaults({ shouldComponentUpdate: shouldUpdate }); expect(localComponent.shouldComponentUpdate).toBe(shouldUpdate); expect(localComponent.name).toBe('ComponentCreator'); var Component = localComponent(function() { expect(this.shouldComponentUpdate).toBe(shouldUpdate); done(); return textNode('hello'); }); render(Component({ foo: 'hello' })); }); test('should have debug on product of withDefaults', () => { var shouldUpdate = function() { return true; }; var localComponent = component.withDefaults({ shouldComponentUpdate: shouldUpdate }); expect(typeof localComponent.debug).toBe('function'); }); test('should have overridable isCursor', done => { var isCursor = function() { return done(); }; var localComponent = component.withDefaults({ isCursor: isCursor }); expect(localComponent.shouldComponentUpdate.isCursor).toBe(isCursor); var Component = localComponent(function() { return textNode('hello'); }); render(Component({ foo: 'hello' })); }); test('should have overridable isImmutable', done => { var isImmutable = function() { return done(); }; var localComponent = component.withDefaults({ isImmutable: isImmutable }); expect(localComponent.shouldComponentUpdate.isImmutable).toBe( isImmutable ); var Component = localComponent(function() { return textNode('hello'); }); render(Component({ foo: 'hello' })); }); test('should have overridable cursorField', () => { var localComponent = component.withDefaults({ cursorField: 'cursor' }); var cursor1 = {}; var Component = localComponent(function(cursor) { expect(cursor).toBe(cursor1); return textNode('hello'); }); render(React.createElement(Component, { cursor: cursor1 })); }); }); describe('exposes arguments as props', () => { test('should expose single cursor', done => { var mixins = [{ componentDidMount: () => done() }]; var props = { cursor: 'cursor' }; var Component = component(mixins, function() { expect(this.props.cursor).toBe(props.cursor); return textNode('hello'); }); render(Component(props)); }); test('should expose multiple cursors', done => { var mixins = [{ componentDidMount: () => done() }]; var cursor1 = {}; var cursor2 = {}; var Component = component(mixins, function() { expect(this.props.one).toBe(cursor1); expect(this.props.two).toBe(cursor2); return textNode('hello'); }); render(Component({ one: cursor1, two: cursor2 })); }); }); describe('creator disguises as a react class', () => { test('contains `isReactComponent` in its prototype chain', function() { var Component = component(function() { return React.createElement('div'); }); expect(Component.prototype.isReactComponent).not.toBeFalsy(); }); test('creates react class instance, not an element, when passed `publicProps`, `publicContext`, and `ReactUpdateQueue`', () => { var Component = component(function() { return React.createElement('div'); }); expect(React.isValidElement(Component())).toBe(true); expect(React.isValidElement(new Component({}, {}))).toBe(false); }); test('exposes `type` on itself', () => { var Type; var comp = component.classDecorator(Class => (Type = Class)); var Creator = comp(function() { return React.createElement('div'); }); expect(Creator.type).toBe(Type); }); test('will pass default props', () => { var expectedPropValue = 'default-prop-value'; var lifecycleMethods = { getDefaultProps: function() { return { direction: expectedPropValue }; } }; var Component = component(lifecycleMethods, function(props) { expect(props.direction).toBe(expectedPropValue); expect(this.props.direction).toBe(expectedPropValue); return React.createElement('div'); }); render(Component()); render(React.createElement(Component)); // jsx }); }); describe('should not re-render', () => { test('should not rerender on equivalent input', () => { var rendered = 0; var Component = component(function(input) { rendered = rendered + 1; return textNode('Rendered ' + rendered + ' times'); }); render(Component({})); expect(rendered).toBe(1); render(Component({})); expect(rendered).toBe(1); }); }); test('passing componentWillReceiveProps as mixin', done => { var willReceivePropsCalled = 0; var renderCalled = 0; var mixin = { componentWillReceiveProps: function(props) { willReceivePropsCalled = willReceivePropsCalled + 1; } }; var Component = component([mixin], function(input, output) { renderCalled = renderCalled + 1; return textNode('hello'); }); var onChange = function() { return 1; }; render(Component({}, { onChange: onChange })); expect(renderCalled).toBe(1); render(Component({}, { onChange: onChange })); expect(renderCalled).toBe(1); expect(willReceivePropsCalled).toBe(1); render(Component({}, { onChange: onChange })); expect(renderCalled).toBe(1); expect(willReceivePropsCalled).toBe(2); render( Component( { a: 1 }, { onChange: function() { return 4; } } ) ); expect(renderCalled).toBe(2); expect(willReceivePropsCalled).toBe(3); done(); }); test('passing componentWillMount as mixin', done => { var willMountCalled = 0; var renderCalled = 0; var mixin = { componentWillMount: function() { willMountCalled = willMountCalled + 1; } }; var Component = component([mixin], function(input, output) { renderCalled = renderCalled + 1; return textNode('hello'); }); var onChange = function() { return 1; }; render(Component({}, { onChange: onChange })); expect(renderCalled).toBe(1); expect(willMountCalled).toBe(1); render(Component({}, { onChange: onChange })); expect(renderCalled).toBe(1); expect(willMountCalled).toBe(1); onChange = function() { return 3; }; render(Component({ a: 1 }, { onChange: onChange })); expect(renderCalled).toBe(2); expect(willMountCalled).toBe(1); render(Component({ a: 1 }, { onChange: onChange })); expect(renderCalled).toBe(2); expect(willMountCalled).toBe(1); done(); }); var root; beforeEach(() => { root = document.createElement('div'); }); function render(component) { ReactDOM.render(component, root); } }); function noop() {} /** * Interop between React 16.x (can return strings directly in a component's `render` method) * and React <16 (must return a `text` element from a component's `render` method) * @param {string} textContent */ function textNode(textContent) { return React.DOM && React.DOM.text ? React.createElement('text', { children: textContent }) : textContent; } function hasKey(component, key) { var element = component._currentElement; if (component._reactInternalFiber) { element = component._reactInternalFiber; } else if ( component._reactInternalInstance && component._reactInternalInstance._currentElement ) { element = component._reactInternalInstance._currentElement; } expect(element.key).toBe(key); }