Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for unserializable props in fixture #15

Closed
wants to merge 13 commits into from
Closed
23 changes: 21 additions & 2 deletions fixtures/component-playground/default.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
var React = require('react');

class FirstComponent extends React.Component {
render() {
return React.DOM.div();
}
}

class SecondComponent extends React.Component {
render() {
return React.DOM.div();
}
}

module.exports = {
components: {
FirstComponent: {
class: 'FirstComponent',
class: FirstComponent,
fixtures: {
'default': {
myProp: false,
nested: {
foo: 'bar',
shouldBeCloned: {}
},
children: [
React.createElement('span', {
children: 'test child'
})
],
state: {
somethingHappened: false
}
Expand All @@ -17,7 +36,7 @@ module.exports = {
}
},
SecondComponent: {
class: 'SecondComponent',
class: SecondComponent,
fixtures: {
'index': {
myProp: true,
Expand Down
10 changes: 9 additions & 1 deletion fixtures/component-playground/selected-fixture.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var _ = require('lodash'),
var React = require('react'),
_ = require('lodash'),
defaultFixture = require('./default.js');

module.exports = _.merge({}, defaultFixture, {
Expand All @@ -17,6 +18,13 @@ module.exports = _.merge({}, defaultFixture, {
somethingHappened: false
}
},
fixtureUnserializableProps: {
children: [
React.createElement('span', {
children: 'test child'
})
]
},
fixtureChange: 10
}
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dependencies": {
"classnames": "^1.2.0",
"lodash": "^3.6.0",
"react-component-tree": "^0.2.2",
"react-component-tree": "^0.2.3",
"react-querystring-router": "^0.2.0"
},
"devDependencies": {
Expand Down
42 changes: 29 additions & 13 deletions src/components/component-playground.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ var _ = require('lodash'),
classNames = require('classnames'),
ComponentTree = require('react-component-tree'),
stringifyParams = require('react-querystring-router').uri.stringifyParams,
parseLocation = require('react-querystring-router').uri.parseLocation;
parseLocation = require('react-querystring-router').uri.parseLocation,
isSerializable = require('../lib/is-serializable.js').isSerializable;

module.exports = React.createClass({
/**
Expand Down Expand Up @@ -62,15 +63,31 @@ module.exports = React.createClass({
expandedComponents:
this.getExpandedComponents(props, expandedComponents),
fixtureContents: {},
fixtureUnserializableProps: {},
fixtureUserInput: '{}',
isFixtureUserInputValid: true
};

if (this.isFixtureSelected(props)) {
var fixtureContents = this.getSelectedFixtureContents(props);
var originalFixtureContents = this.getSelectedFixtureContents(props),
fixtureContents = {},
fixtureUnserializableProps = {};

// Unserializable props are stored separately from serializable ones
// because the serializable props can be overriden by the user using
// the editor, while the unserializable props are always attached
// behind the scenes
_.forEach(originalFixtureContents, function(value, key) {
if (isSerializable(value)) {
fixtureContents[key] = value;
} else {
fixtureUnserializableProps[key] = value;
}
});

_.assign(state, {
fixtureContents: fixtureContents,
fixtureUnserializableProps: fixtureUnserializableProps,
fixtureUserInput: this.getStringifiedFixtureContents(fixtureContents)
});
}
Expand Down Expand Up @@ -104,6 +121,9 @@ module.exports = React.createClass({
key: this._getPreviewComponentKey()
};

// Shallow apply unserializable props
_.assign(params, this.state.fixtureUnserializableProps);

return _.merge(params, _.omit(this.state.fixtureContents, 'state'));
}
},
Expand Down Expand Up @@ -357,10 +377,14 @@ module.exports = React.createClass({

var snapshot = ComponentTree.serialize(this.refs.preview);

// Continue to ignore unserializable props
var serializableSnapshot =
_.omit(snapshot, _.keys(this.state.fixtureUnserializableProps));

this.setState({
fixtureContents: snapshot,
fixtureContents: serializableSnapshot,
fixtureUserInput:
this.constructor.getStringifiedFixtureContents(snapshot),
this.constructor.getStringifiedFixtureContents(serializableSnapshot),
isFixtureUserInputValid: true
});
},
Expand All @@ -370,15 +394,7 @@ module.exports = React.createClass({
newState = {fixtureUserInput: userInput};

try {
var originalFixtureContents =
this.constructor.getSelectedFixtureContents(this.props);

// We only want to extend Function props because they can't be serialized
// and they are not part of the editor's contents
var fixtureContents =
_.pick(originalFixtureContents, function(value, key) {
return _.isFunction(value);
});
var fixtureContents = {};

if (userInput) {
_.merge(fixtureContents, JSON.parse(userInput));
Expand Down
24 changes: 24 additions & 0 deletions src/lib/is-serializable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
var _ = require('lodash');

exports.isSerializable = function(obj) {
if (_.isUndefined(obj) ||
_.isNull(obj) ||
_.isBoolean(obj) ||
_.isNumber(obj) ||
_.isString(obj)) {
return true;
}

if (!_.isPlainObject(obj) &&
!_.isArray(obj)) {
return false;
}

for (var key in obj) {
if (!exports.isSerializable(obj[key])) {
return false;
}
}

return true;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,54 @@ describe(`ComponentPlayground (${FIXTURE}) Events Handlers`, function() {
container,
fixture;

var childSnapshot = {},
stringifiedChildSnapshot = '{}',
stateSet;

beforeEach(function() {
sinon.stub(ComponentTree, 'serialize').returns(childSnapshot);
sinon.stub(JSON, 'stringify').returns(stringifiedChildSnapshot);

({fixture, container, component, $component} = render(originalFixture));

sinon.spy(component, 'setState');

component.onFixtureUpdate();
stateSet = component.setState.lastCall.args[0];
});

afterEach(function() {
ComponentTree.serialize.restore();
JSON.stringify.restore();

component.setState.restore();
});

it('should mark user input state as valid', function() {
expect(stateSet.isFixtureUserInputValid).to.equal(true);
});

it('should serialize preview child', function() {
expect(ComponentTree.serialize)
.to.have.been.calledWith(component.refs.preview);
});

it('should update child snapshot state', function() {
expect(stateSet.fixtureContents).to.equal(childSnapshot);
});

it('should stringify preview child snapshot', function() {
expect(JSON.stringify).to.have.been.calledWith(childSnapshot);
});

it('should update stringified child snapshot state', function() {
expect(stateSet.fixtureUserInput).to.equal(stringifiedChildSnapshot);
define('on fixture update', function() {
var stateSet;

beforeEach(function() {
sinon.stub(ComponentTree, 'serialize').returns(_.merge({},
fixture.state.fixtureContents,
fixture.state.fixtureUnserializableProps));
sinon.spy(component, 'setState');

component.onFixtureUpdate();

stateSet = component.setState.lastCall.args[0];
});

afterEach(function() {
ComponentTree.serialize.restore();
component.setState.restore();
});

it('should mark user input state as valid', function() {
expect(stateSet.isFixtureUserInputValid).to.equal(true);
});

it('should serialize preview child', function() {
expect(ComponentTree.serialize)
.to.have.been.calledWith(component.refs.preview);
});

it('should put serializable child state in fixture contents', function() {
for (var key in fixture.state.fixtureContents) {
expect(stateSet.fixtureContents[key])
.to.deep.equal(fixture.state.fixtureContents[key]);
}
});

it('should ignore unserializable child state', function() {
for (var key in fixture.state.fixtureUnserializableProps) {
expect(stateSet.fixtureContents[key]).to.be.undefined;
}
});

it('should update stringified child snapshot state', function() {
expect(stateSet.fixtureUserInput)
.to.equal(JSON.stringify(fixture.state.fixtureContents, null, 2));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe(`ComponentPlayground (${FIXTURE}) Events DOM`, function() {

expect(stateSet.expandedComponents.length).to.equal(1);
expect(stateSet.expandedComponents[0]).to.equal('FirstComponent');
expect(stateSet.fixtureContents).to.equal(fixtureContents);
expect(stateSet.fixtureContents).to.deep.equal(fixtureContents);
expect(stateSet.fixtureUserInput).to.equal(
JSON.stringify(fixtureContents, null, 2));
expect(stateSet.isFixtureUserInputValid).to.equal(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ describe(`ComponentPlayground (${FIXTURE}) Render Children`, function() {

it('should send fixture contents to preview child', function() {
var fixtureContents = fixture.state.fixtureContents;

for (var key in fixtureContents) {
if (key !== 'state') {
expect(childParams[key]).to.deep.equal(fixtureContents[key]);
}
}
});

it('should send unserializable props to preview child', function() {
var fixtureUnserializableContents =
fixture.state.fixtureUnserializableContents;

for (var key in fixtureUnserializableContents) {
expect(childParams[key]).to.equal(fixtureUnserializableContents[key]);
}
});

it('should not send state as prop to preview child', function() {
expect(childParams.state).to.be.undefined;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ describe(`ComponentPlayground (${FIXTURE}) Transitions Mount`, function() {
expect(expandedComponents[0]).to.equal('FirstComponent');
});

it('should populate state with fixture contents', function() {
it('should populate state with serializable fixture contents', function() {
expect(component.state.fixtureContents.myProp).to.equal(false);
});

it('should populate state with unserializable fixture props', function() {
expect(component.state.fixtureUnserializableProps.children).to.equal(
fixture.children);
});

it('should populate stringified fixture contents as user input', function() {
expect(component.state.fixtureUserInput).to.equal(
JSON.stringify(component.state.fixtureContents, null, 2));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ describe(`ComponentPlayground (${FIXTURE}) Transitions Props`, function() {
expect(stateSet.fixtureContents.myProp).to.equal(true);
});

it('should reset unserializable fixture props', function() {
expect(stateSet.fixtureUnserializableProps).to.deep.equal({});
});

it('should replace fixture user input', function() {
expect(JSON.parse(stateSet.fixtureUserInput).myProp).to.equal(true);
});
Expand Down