jasmine-react-helpers
Advanced tools
Comparing version 0.1.2 to 0.2.0
{ | ||
"name": "jasmine-react-helpers", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "a small suite of helper functions to make unit testing React.js components painless.", | ||
@@ -5,0 +5,0 @@ "main": "src/jasmine-react.js", |
@@ -133,7 +133,13 @@ *jasmine-react* is a small suite of helper functions to make unit testing React.js components painless. | ||
When rendering a React component, this is a convenience function for React.renderComponent. | ||
When rendering a React component, this is a convenience function for `React.renderComponent`. | ||
It has a few helpful features: | ||
* the container argument is optional. By default it will be: `document.getElementById("jasmine_content") | ||
* the component is actually rendered to an attached DOM node (unlike `React.addons.TestUtils.renderIntoDocument which | ||
renders into a detached DOM node). | ||
* the component will be automatically unmounted after the test is complete. | ||
NOTE: If you call React.renderComponent in a jasmine test and the component is not unmounted, that component | ||
will pollute any subsequent tests which try to render into that container. | ||
* the container argument is optional. By default it will be: `document.getElementById("jasmine_content"). If you | ||
want to override this behavior, look at the documentation for `jasmineReact.getDefaultContainer` | ||
* `React.renderComponent` will return before the rendering has occurred. `jasmineReact.renderComponent` will wait | ||
@@ -211,14 +217,10 @@ until the async render has been performed. | ||
## jasmineReact.getJasmineContent | ||
## jasmineReact.getDefaultContainer | ||
After each test, it is imperative that jasmineReact clean up after itself so that one test doesn't pollute the next. | ||
One step in this process, is making sure that any component rendered gets unmounted. By default, in a jasmine suite | ||
all DOM elements should be a child of the `#jasmine_content` div. If that is true for you, then you won't need to use | ||
this function. But if you use some other div to render your jasmine DOM, then you'll want to redefine this function to | ||
meet your specification. | ||
The default container for jasmineReact is `document.getElementById("jasmine_content")`. | ||
If your jasmine test page, uses `#spec-dom` as its dom node, then you'd want to define the following: | ||
If your jasmine test page uses `#spec-dom` as its default dom node, then you'd want to define the following: | ||
```js | ||
jasmineReact.getJasmineContent = function(){ | ||
jasmineReact.getDefaultContainer = function(){ | ||
return document.getElementById("spec-dom"); | ||
@@ -230,5 +232,11 @@ }; | ||
To install, include the `src/jasmine-react.js` file in your jasmine spec helpers list. | ||
``` | ||
npm install jasmine-react-helpers --save-dev | ||
``` | ||
Bower: TODO | ||
Script Tag: TODO | ||
# Testing | ||
@@ -235,0 +243,0 @@ |
var React = require('react'); | ||
var spies = [], | ||
componentStubs = [], | ||
renderedComponents = []; | ||
var jasmineReact = { | ||
renderComponent: function(component, container, callback){ | ||
if(typeof container === "undefined"){ | ||
container = jasmineReact.getJasmineContent(); | ||
container = this.getDefaultContainer(); | ||
} | ||
@@ -13,2 +17,5 @@ | ||
// keep track of the components we render, so we can unmount them later | ||
renderedComponents.push(comp); | ||
return comp; | ||
@@ -18,8 +25,10 @@ }, | ||
spyOnClass: function(klass, methodName){ | ||
var klassProto = jasmineReact.classPrototype(klass), | ||
var klassProto = this.classPrototype(klass), | ||
jasmineSpy = spyOn(klassProto, methodName); | ||
this.jasmineReactSpies_ = this.jasmineReactSpies_ || []; | ||
this.jasmineReactSpies_.push(jasmineSpy); | ||
// keep track of the spies, so we can clean up the __reactAutoBindMap later | ||
spies.push(jasmineSpy); | ||
// react.js will autobind `this` to the correct value and it caches that | ||
// result on a __reactAutoBindMap for performance reasons. | ||
if(klassProto.__reactAutoBindMap){ | ||
@@ -38,3 +47,3 @@ klassProto.__reactAutoBindMap[methodName] = jasmineSpy; | ||
classPrototype: function(klass){ | ||
var componentConstructor = jasmineReact.classComponentConstructor(klass); | ||
var componentConstructor = this.classComponentConstructor(klass); | ||
@@ -49,4 +58,4 @@ if(typeof componentConstructor === "undefined"){ | ||
createStubComponent: function(obj, propertyName){ | ||
this.jasmineReactComponentStubs_ = this.jasmineReactComponentStubs_ || []; | ||
this.jasmineReactComponentStubs_.push({obj: obj, propertyName: propertyName, originalValue: obj[propertyName]}); | ||
// keep track of the components we stub, so we can swap them back later | ||
componentStubs.push({obj: obj, propertyName: propertyName, originalValue: obj[propertyName]}); | ||
@@ -64,3 +73,3 @@ return obj[propertyName] = React.createClass({ | ||
} | ||
jasmineReact.classPrototype(klass)[methodName] = methodDefinition; | ||
this.classPrototype(klass)[methodName] = methodDefinition; | ||
return klass; | ||
@@ -70,21 +79,13 @@ }, | ||
resetComponentStubs: function(){ | ||
if(!this.jasmineReactComponentStubs_){ | ||
return; | ||
} | ||
for (var i = 0; i < this.jasmineReactComponentStubs_.length; i++) { | ||
var stub = this.jasmineReactComponentStubs_[i]; | ||
for (var i = 0; i < componentStubs.length; i++) { | ||
var stub = componentStubs[i]; | ||
stub.obj[stub.propertyName] = stub.originalValue; | ||
} | ||
this.jasmineReactComponentStubs_ = []; | ||
componentStubs = []; | ||
}, | ||
removeAllSpies: function(){ | ||
if(!this.jasmineReactSpies_){ | ||
return; | ||
} | ||
for (var i = 0; i < this.jasmineReactSpies_.length; i++) { | ||
var spy = this.jasmineReactSpies_[i]; | ||
for (var i = 0; i < spies.length; i++) { | ||
var spy = spies[i]; | ||
if(spy.baseObj.__reactAutoBindMap){ | ||
@@ -96,26 +97,23 @@ spy.baseObj.__reactAutoBindMap[spy.methodName] = spy.originalValue; | ||
this.jasmineReactSpies_ = []; | ||
spies = []; | ||
}, | ||
unmountComponent: function(component){ | ||
return React.unmountComponentAtNode(component.getDOMNode().parentNode); | ||
unmountAllRenderedComponents: function(){ | ||
for (var i = 0; i < renderedComponents.length; i++) { | ||
var renderedComponent = renderedComponents[i]; | ||
this.unmountComponent(renderedComponent); | ||
} | ||
renderedComponents = []; | ||
}, | ||
clearJasmineContent: function(){ | ||
var jasmineContentEl = jasmineReact.getJasmineContent(); | ||
if(jasmineContentEl){ | ||
React.unmountComponentAtNode(jasmineContentEl); | ||
jasmineContentEl.innerHTML = ""; | ||
unmountComponent: function(component){ | ||
if(component.isMounted()){ | ||
return React.unmountComponentAtNode(component.getDOMNode().parentNode); | ||
} else { | ||
var warningMessage = "jasmineReact is unable to clear out the jasmine content element, because it could not find an " + | ||
"element with an id of 'jasmine_content'. " + | ||
"This may result in bugs, because a react component which isn't unmounted may pollute other tests " + | ||
"and cause test failures/weirdness. " + | ||
"If you'd like to override this behavior to look for a different element, then implement a method like this: " + | ||
"jasmineReact.getJasmineContent = function(){ return document.getElementById('foo'); };" | ||
console.warn(warningMessage); | ||
return false; | ||
} | ||
}, | ||
getJasmineContent: function(){ | ||
getDefaultContainer: function(){ | ||
return document.getElementById("jasmine_content"); | ||
@@ -125,8 +123,9 @@ } | ||
// TODO: this has no automated test coverage. Add some integration tests for coverage. | ||
afterEach(function(){ | ||
jasmineReact.removeAllSpies(); | ||
jasmineReact.resetComponentStubs(); | ||
jasmineReact.clearJasmineContent(); | ||
jasmineReact.unmountAllRenderedComponents(); | ||
}); | ||
module.exports = jasmineReact; |
@@ -10,34 +10,33 @@ describe("jasmineReact", function(){ | ||
describe("renderComponent", function(){ | ||
var fakeComponentSpy, fakeContainerSpy, fakeRenderComponentResponseSpy; | ||
var fooKlass; | ||
beforeEach(function(){ | ||
fakeComponentSpy = jasmine.createSpy("fakeComponent"); | ||
fakeContainerSpy = jasmine.createSpy("fakeContainer"); | ||
fooKlass = React.createClass({ | ||
render: function(){ | ||
return React.DOM.div({}); | ||
} | ||
}); | ||
fakeRenderComponentResponseSpy = jasmine.createSpy("fakeRenderComponentResponse"); | ||
spyOn(React, "renderComponent").andReturn(fakeRenderComponentResponseSpy); | ||
spyOn(React, "renderComponent").andCallThrough(); | ||
}); | ||
it("should call React.renderComponent", function(){ | ||
jasmineReact.renderComponent(); | ||
it("should call React.renderComponent with the passed in component", function(){ | ||
jasmineReact.renderComponent(fooKlass({foo: "bar"}), document.getElementById("jasmine_content")); | ||
expect(React.renderComponent).toHaveBeenCalled(); | ||
}); | ||
var renderComponentArgs = React.renderComponent.mostRecentCall.args[0]; | ||
it("should call React.renderComponent with the passed in component", function(){ | ||
jasmineReact.renderComponent(fakeComponentSpy, fakeContainerSpy); | ||
expect(React.renderComponent).toHaveBeenCalledWith(fakeComponentSpy, jasmine.any(Function)); | ||
expect(renderComponentArgs.props.foo).toBe("bar"); | ||
}); | ||
it("should call React.renderComponent with the passed in container", function(){ | ||
jasmineReact.renderComponent(fakeComponentSpy, fakeContainerSpy); | ||
var container = document.getElementById("jasmine_content"); | ||
jasmineReact.renderComponent(fooKlass(), container); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Function), fakeContainerSpy); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Object), container); | ||
}); | ||
it("should call React.renderComponent with #jasmine_content container if no container is passed in", function(){ | ||
jasmineReact.renderComponent(fakeComponentSpy); | ||
jasmineReact.renderComponent(fooKlass()); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Function), document.getElementById("jasmine_content")); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Object), document.getElementById("jasmine_content")); | ||
}); | ||
@@ -48,14 +47,37 @@ | ||
jasmineReact.renderComponent(fakeComponentSpy, fakeContainerSpy, fakeCallbackSpy); | ||
jasmineReact.renderComponent(fooKlass(), document.getElementById("jasmine_content"), fakeCallbackSpy); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), fakeCallbackSpy); | ||
expect(React.renderComponent).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Object), fakeCallbackSpy); | ||
}); | ||
it("should return the return value of React.renderComponent", function(){ | ||
var returnValue = jasmineReact.renderComponent(fakeComponentSpy, fakeContainerSpy); | ||
var returnValue = jasmineReact.renderComponent(fooKlass({baz: "bat"}), document.getElementById("jasmine_content")); | ||
expect(returnValue).toBe(fakeRenderComponentResponseSpy); | ||
expect(returnValue.props.baz).toBe("bat"); | ||
}); | ||
}); | ||
describe("renderComponent: test pollution", function(){ | ||
it("should not pollute a rendered component from one test into another test", function(){ | ||
var coolKlass = React.createClass({ | ||
render: function(){ | ||
return React.DOM.div({ | ||
id: "really-cool" | ||
}); | ||
} | ||
}); | ||
// lets pretend this is test #1 | ||
jasmineReact.renderComponent(coolKlass()); | ||
expect(document.getElementById("really-cool")).toBeDefined(); | ||
// this is the method in the afterEach which is needed to prevent test pollution for renderComponent | ||
jasmineReact.unmountAllRenderedComponents(); | ||
// lets pretend this is test #1 | ||
expect(document.getElementById("really-cool")).toBeNull(); | ||
}); | ||
}); | ||
describe("spyOnClass", function(){ | ||
@@ -182,3 +204,3 @@ var fooKlass; | ||
jasmineReact.removeAllSpies(); | ||
jasmineReact.clearJasmineContent(); | ||
jasmineReact.unmountAllRenderedComponents(); | ||
@@ -340,71 +362,55 @@ // lets pretend this is test #2 | ||
it("should unmount the component", function(){ | ||
var barComponent = jasmineReact.renderComponent(barKlass()); | ||
expect(componentWillUnmountSpy.callCount).toBe(0); | ||
describe("the component is mounted", function(){ | ||
it("should unmount the component", function(){ | ||
var barComponent = jasmineReact.renderComponent(barKlass()); | ||
expect(componentWillUnmountSpy.callCount).toBe(0); | ||
jasmineReact.unmountComponent(barComponent); | ||
jasmineReact.unmountComponent(barComponent); | ||
expect(componentWillUnmountSpy.callCount).toBe(1); | ||
}); | ||
expect(componentWillUnmountSpy.callCount).toBe(1); | ||
}); | ||
it("should return the return value of unmountComponentAtNode", function(){ | ||
var fakeUnmount = jasmine.createSpy("unmountComponentAtNode"); | ||
spyOn(React, "unmountComponentAtNode").andReturn(fakeUnmount); | ||
it("should return the return value of unmountComponentAtNode", function(){ | ||
var barComponent = jasmineReact.renderComponent(barKlass({cool: "town"})); | ||
var barComponent = jasmineReact.renderComponent(barKlass()); | ||
var returnValue = jasmineReact.unmountComponent(barComponent); | ||
expect(jasmineReact.unmountComponent(barComponent)).toBe(fakeUnmount); | ||
expect(returnValue).toBe(true); | ||
}); | ||
}); | ||
}); | ||
describe("clearJasmineContent", function(){ | ||
it("should clear out the html in #jasmine_content", function(){ | ||
var barKlass = React.createClass({ | ||
render: function(){ | ||
return React.DOM.div({id: "bar-test-div"}); | ||
} | ||
}); | ||
describe("the component is not mounted", function(){ | ||
jasmineReact.renderComponent(barKlass()); | ||
expect(document.getElementById("bar-test-div")).toBeDefined(); | ||
it("should not unmount the component", function(){ | ||
var barComponent = jasmineReact.renderComponent(barKlass()); | ||
jasmineReact.clearJasmineContent(); | ||
React.unmountComponentAtNode(barComponent.getDOMNode().parentNode); | ||
expect(document.getElementById("bar-test-div")).toBeNull(); | ||
}); | ||
expect(componentWillUnmountSpy.callCount).toBe(1); | ||
it("should unmount the react component in #jasmine_content", function(){ | ||
var componentWillUnmountSpy = jasmine.createSpy("componentWillUnmount"); | ||
expect(function(){ | ||
jasmineReact.unmountComponent(barComponent); | ||
}).not.toThrow(); | ||
var barKlass = React.createClass({ | ||
render: function(){ | ||
return React.DOM.div({id: "bar-test-div"}); | ||
}, | ||
componentWillUnmount: function(){ | ||
componentWillUnmountSpy(); | ||
} | ||
expect(componentWillUnmountSpy.callCount).toBe(1); | ||
}); | ||
jasmineReact.renderComponent(barKlass()); | ||
expect(componentWillUnmountSpy.callCount).toBe(0); | ||
it("should return false", function(){ | ||
var barComponent = jasmineReact.renderComponent(barKlass()); | ||
jasmineReact.clearJasmineContent(); | ||
React.unmountComponentAtNode(barComponent.getDOMNode().parentNode); | ||
expect(componentWillUnmountSpy.callCount).toBe(1); | ||
}); | ||
var returnValue; | ||
it("should warn the user if a jasmine content element can't be found", function(){ | ||
spyOn(jasmineReact, "getJasmineContent").andCallFake(function(){ | ||
return document.getElementById("bogus"); | ||
expect(function(){ | ||
returnValue = jasmineReact.unmountComponent(barComponent); | ||
}).not.toThrow(); | ||
expect(returnValue).toBe(false); | ||
}); | ||
}); | ||
spyOn(console, "warn"); | ||
jasmineReact.clearJasmineContent(); | ||
expect(console.warn).toHaveBeenCalled(); | ||
expect(console.warn.mostRecentCall.args[0].substring(0, 63)).toBe("jasmineReact is unable to clear out the jasmine content element"); | ||
}); | ||
}); | ||
}); |
261
34809