├── .gitignore ├── CONTRIBUTING.md ├── Gruntfile.js ├── MIT-LICENSE ├── README.md ├── package.json ├── src └── jasmine-react.js └── test ├── documentation-spec.js ├── jasmine-react-spec.js └── support ├── jasmine-content.js ├── phantomjs-shims.js └── react.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | build 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | * Make sure the tests are green. 4 | * Add tests for your new features/fixes, so I don't break them in the future. 5 | * Add documentation to the README, so people know about your new feature. -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | 6 | karma: { 7 | options: { 8 | basePath: '', 9 | 10 | frameworks: ['jasmine', 'browserify'], 11 | 12 | preprocessors: { 13 | 'test/support/react.js': ['browserify'], 14 | 'test/documentation-spec.js': ['react-jsx'], 15 | 'test/jasmine-react-spec.js': ['react-jsx'], 16 | 'src/jasmine-react.js': ['browserify'] 17 | }, 18 | 19 | files: [ 20 | 'test/support/**/*.js', 21 | 'src/jasmine-react.js', 22 | 'test/**/*-spec.js' 23 | ], 24 | 25 | reporters: ['progress'] 26 | }, 27 | 28 | dev: { 29 | browsers: ['Firefox', 'Chrome', 'PhantomJS'], 30 | autoWatch: true 31 | }, 32 | 33 | chrome: { 34 | browsers: ['Chrome'], 35 | autoWatch: true 36 | }, 37 | 38 | firefox: { 39 | browsers: ['Firefox'], 40 | autoWatch: true 41 | }, 42 | 43 | phantomjs: { 44 | browsers: ['PhantomJS'], 45 | autoWatch: true 46 | }, 47 | 48 | unit: { 49 | browsers: ['PhantomJS'], 50 | autoWatch: false, 51 | singleRun: true 52 | } 53 | } 54 | }); 55 | 56 | grunt.loadNpmTasks('grunt-karma'); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tom Hallett 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *jasmine-react* is a small suite of helper functions to make unit testing React.js components painless. 2 | 3 | # Why do I need jasmine-react.js? 4 | 5 | React.js' architecture is based on the idea that mutation is hard, so your React.js components should represent a state machine which can represent your UI under any state. This is an extremely powerful idea and means that you will often compose components inside of other components. While this is great for code reuse, it makes isolating one component for a unit test slightly more difficult. React.js will also provide some very helpful features (like auto binding), which make it more difficult to get under the covers and stub things out. jasmine-react aims to solve these testing issues. 6 | 7 | # Synopsis 8 | 9 | Here's an overview of how jasmineReact can be used: 10 | 11 | ### Spying on a method in a React Class 12 | 13 | ```javascript 14 | /** @jsx React.DOM */ 15 | window.HelloWorld = React.createClass({ 16 | getInitialState: function(){ 17 | return { number: this.randomNumber() }; 18 | }, 19 | randomNumber: function(){ 20 | return Math.random(); 21 | }, 22 | render: function() { 23 | return (
Hello {this.state.number}
); 24 | } 25 | }); 26 | 27 | describe("HelloWorld", function(){ 28 | it("can spy on a function for a React class", function(){ 29 | jasmineReact.spyOnClass(HelloWorld, "randomNumber").andReturn(42); 30 | 31 | // jasmineReact wraps React.render, so you don't have to worry 32 | // about the async nature of when the actual DOM get's rendered, or selecting 33 | // where your component needs to get rendered (default is #jasmine_content) 34 | var myWorld = jasmineReact.render(); 35 | 36 | expect(myWorld.state.number).toBe(42); 37 | }); 38 | 39 | it("can assert that a spy has been called", function(){ 40 | jasmineReact.spyOnClass(HelloWorld, "randomNumber"); 41 | 42 | jasmineReact.render(; 43 | 44 | // because we spy on the class and not the instance, we have to assert that the 45 | // function on the class' prototype was called. 46 | expect(jasmineReact.classPrototype(HelloWorld).randomNumber).toHaveBeenCalled(); 47 | }); 48 | }); 49 | ``` 50 | 51 | ### Replacing a component's subcomponent with a test double 52 | 53 | This is very helpful for isolating your component tests to just that component, and avoiding 54 | testing its subcomponents. 55 | 56 | ```javascript 57 | /** @jsx React.DOM */ 58 | window.Avatar = React.createClass({ 59 | render: function() { 60 | return ( 61 |
62 | 63 |
64 | ); 65 | } 66 | }); 67 | 68 | window.Profile = React.createClass({ 69 | render: function(){ 70 | throw("I like to blow up"); 71 | } 72 | }); 73 | 74 | describe("Avatar", function(){ 75 | 76 | it("should spy on a subcomponent and use a test double component", function(){ 77 | jasmineReact.createStubComponent(window, "Profile"); 78 | 79 | // This line won't throw the "I like to blow up" error because we've replaced the class with a test double! 80 | var avatar = jasmineReact.render(); 81 | 82 | expect(avatar.refs.pic.props.username).toBe("Zuck") 83 | }); 84 | 85 | }); 86 | ``` 87 | 88 | ### Adding a method to a component class 89 | 90 | This is needed to make a test double implement an interface which the component under test requires. 91 | 92 | ```javascript 93 | /** @jsx React.DOM */ 94 | window.Avatar = React.createClass({ 95 | render: function() { 96 | return ( 97 |
98 | 99 |
100 | ); 101 | }, 102 | 103 | rotateProfile: function(){ 104 | this.refs.pic.rotate(); 105 | } 106 | }); 107 | 108 | describe("Avatar", function(){ 109 | describe("rotateProfile", function(){ 110 | it("should call 'rotate' on the Profile subcomponent", function(){ 111 | var profileClassStub = jasmineReact.createStubComponent(window, "Profile"); 112 | 113 | // We could also do: jasmineReact.addMethodToClass(window.Profile, "rotate", function(){}); 114 | jasmineReact.addMethodToClass(profileClassStub, "rotate", function(){}); 115 | jasmineReact.spyOnClass(profileClassStub, "rotate"); 116 | 117 | var avatar = jasmineReact.render(); 118 | 119 | expect(jasmineReact.classPrototype(profileClassStub).rotate).not.toHaveBeenCalled(); 120 | avatar.rotateProfile(); 121 | expect(jasmineReact.classPrototype(profileClassStub).rotate).toHaveBeenCalled(); 122 | }); 123 | }); 124 | }); 125 | ``` 126 | 127 | # API 128 | 129 | ## jasmineReact.render 130 | 131 | `jasmineReact.render(component, [container], [callback]);` 132 | 133 | When rendering a React component, this is a convenience function for `React.render`. 134 | 135 | It has a few helpful features: 136 | 137 | * the component is actually rendered to an attached DOM node (unlike `React.addons.TestUtils.renderIntoDocument which 138 | renders into a detached DOM node). 139 | * the component will be automatically unmounted after the test is complete. 140 | NOTE: If you call React.render in a jasmine test and the component is not unmounted, that component 141 | will pollute any subsequent tests which try to render into that container. 142 | * the container argument is optional. By default it will be: `document.getElementById("jasmine_content"). If you 143 | want to override this behavior, look at the documentation for `jasmineReact.getDefaultContainer` 144 | * `React.render` will return before the rendering has occurred. `jasmineReact.render` will wait 145 | until the async render has been performed. 146 | 147 | Just like `React.render`, this method will return the component instance. 148 | 149 | 150 | ## jasmineReact.spyOnClass 151 | 152 | `jasmineReact.spyOnClass(componentClass, functionName);` 153 | 154 | When you want to render a component and stub on a function for that component, you need to spyOn the function 155 | before the instance has been created because important functions (default props/state, render) happen during initialization. 156 | This means you need to spyOn the component class, not the component instance. 157 | 158 | This function performs the following: 159 | 160 | * uses the vanilla `jasmine.spyOn` to spy on the component class prototype 161 | * React does some performance tricks for [autobinding functions](http://facebook.github.io/react/blog/2013/07/02/react-v0-4-autobind-by-default.html), 162 | so this function will abstract those away from you 163 | * returns a regular jasmine spy object, so you can chain additional spy functions onto it. 164 | For example: `jasmineReact.spyOnClass(Avatar, "getWidth").andCallFake(function(){ return 120; });` 165 | 166 | ## jasmineReact.classPrototype 167 | 168 | `jasmineReact.classPrototype(componentClass)` 169 | 170 | After you've spied on a component class using `jasmineReact.spyOnClass`, you will need to assert things 171 | on that component class. This function returns you the object you want to make your assertions against. 172 | 173 | ```js 174 | jasmineReact.spyOnClass(Avatar, "getWidth"); 175 | 176 | var myAvatar = jasmineReact.render(); 177 | myAvatar.getWidth(); 178 | 179 | expect(jasmineReact.classPrototype(Avatar).getWidth).toHaveBeenCalled(); 180 | 181 | // NOTE: your jasmine-fu will want todo this, but you can't: 182 | // expect(myAvatar.getWidth).toHaveBeenCalled(); <-- DON'T DO THIS 183 | ``` 184 | 185 | ## jasmineReact.createStubComponent 186 | 187 | `jasmineReact.createStubComponent(namespace, className)` 188 | 189 | React components are intended to be composable (using one component inside a render function of another component). While this is great for code reuse, it makes isolating one component for a unit test slightly more difficult. 190 | 191 | *Aside: Why do I want to isolate the component I'm testing from its subcomponents? In a unit test, when you test one component you do want to have to test the behavior of a subcomponent, because that would turn into an integration test.* 192 | 193 | What you want todo is replace any subcomponent's real definition with a "test double". By default this stub component has only the miniumum behavior to be a valid React component: a render function which returns a dom node. 194 | 195 | If you want to add behavior to this stubComponent, so it confirms to the interface of the real component class, use the `jasmineReact.addMethodToClass` function. 196 | 197 | Let's say you have an avatar class named `ProfilePic` which is defined on the global namespace, `window`. 198 | To replace window.ProfilePic with a stub component (for the life of the jasmine test), you would do: 199 | 200 | ```js 201 | jasmineReact.createStubComponent(window, "ProfilePic"); 202 | ``` 203 | 204 | ## jasmineReact.unmountComponent 205 | 206 | `jasmineReact.unmountComponent(component);` 207 | 208 | This function makes it easy to unmount a component, given just the component instance. 209 | Unmounting a component is needed to test `componentWillUnmount` behavior. 210 | 211 | ```js 212 | var myAvatar = jasmineReact.render(); 213 | jasmineReact.unmountComponent(myAvatar); 214 | ``` 215 | 216 | ## jasmineReact.getDefaultContainer 217 | 218 | The default container for jasmineReact is `document.getElementById("jasmine_content")`. 219 | 220 | If your jasmine test page uses `#spec-dom` as its default dom node, then you'd want to define the following: 221 | 222 | ```js 223 | jasmineReact.getDefaultContainer = function(){ 224 | return document.getElementById("spec-dom"); 225 | }; 226 | ``` 227 | 228 | # Installation 229 | 230 | ``` 231 | npm install jasmine-react-helpers --save-dev 232 | ``` 233 | 234 | Bower: TODO 235 | 236 | Script Tag: TODO 237 | 238 | 239 | # Testing 240 | 241 | Install node, npm, and grunt. 242 | 243 | To run all of the tests (Chrome, Firefox, PhantomJS) with autoWatch: 244 | 245 | ```bash 246 | grunt karma 247 | ``` 248 | 249 | To run the tests once with PhantomJS: 250 | 251 | ```bash 252 | grunt karma:unit 253 | ``` 254 | 255 | # TODO 256 | 257 | * Add the following grunt tasks: minification, linting 258 | * Make the project node compatible (https://blog.codecentric.de/en/2014/02/cross-platform-javascript/) 259 | * Create a module on npm and bower 260 | * Add the test suite to Travis CI 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasmine-react-helpers", 3 | "version": "0.2.1", 4 | "description": "a small suite of helper functions to make unit testing React.js components painless.", 5 | "main": "src/jasmine-react.js", 6 | "author": "Tom Hallett", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/tommyh/jasmine-react.git" 10 | }, 11 | "devDependencies": { 12 | "browserify": "^4.2.3", 13 | "grunt": "^0.4.5", 14 | "grunt-karma": "^0.8.3", 15 | "karma": "^0.12.19", 16 | "karma-browserify": "^0.2.1", 17 | "karma-chrome-launcher": "^0.1.4", 18 | "karma-firefox-launcher": "^0.1.3", 19 | "karma-jasmine": "^0.1.5", 20 | "karma-phantomjs-launcher": "^0.1.4", 21 | "karma-react-jsx-preprocessor": "^0.1.1", 22 | "react": "^0.12.1", 23 | "react-tools": "^0.12.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/jasmine-react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var spies = [], 4 | componentStubs = [], 5 | renderedComponents = []; 6 | 7 | var jasmineReact = { 8 | render: function(component, container, callback){ 9 | if(typeof container === "undefined"){ 10 | container = this.getDefaultContainer(); 11 | } 12 | 13 | var comp = (typeof callback === "undefined") ? 14 | React.render(component, container) : 15 | React.render(component, container, callback); 16 | 17 | // keep track of the components we render, so we can unmount them later 18 | renderedComponents.push(comp); 19 | 20 | return comp; 21 | }, 22 | 23 | spyOnClass: function(klass, methodName){ 24 | var klassProto = this.classPrototype(klass), 25 | original = klassProto[methodName], 26 | jasmineSpy = spyOn(klassProto, methodName); 27 | 28 | // keep track of the spies, so we can clean up the __reactAutoBindMap later 29 | // (Jasmine 2.1) 30 | spies.push({ 31 | spy: jasmineSpy, 32 | baseObj: klassProto, 33 | methodName: methodName, 34 | originalValue: original 35 | }); 36 | 37 | // react.js will autobind `this` to the correct value and it caches that 38 | // result on a __reactAutoBindMap for performance reasons. 39 | if(klassProto.__reactAutoBindMap){ 40 | klassProto.__reactAutoBindMap[methodName] = jasmineSpy; 41 | } 42 | 43 | return jasmineSpy; 44 | }, 45 | 46 | classComponentConstructor: function(klass){ 47 | return klass.type || // React 0.11.1 48 | klass.componentConstructor; // React 0.8.0 49 | }, 50 | 51 | classPrototype: function(klass){ 52 | var componentConstructor = this.classComponentConstructor(klass); 53 | 54 | if(typeof componentConstructor === "undefined"){ 55 | throw("A component constructor could not be found for this class. Are you sure you passed in a the component definition for a React component?"); 56 | } 57 | 58 | return componentConstructor.prototype; 59 | }, 60 | 61 | createStubComponent: function(obj, propertyName){ 62 | // keep track of the components we stub, so we can swap them back later 63 | componentStubs.push({obj: obj, propertyName: propertyName, originalValue: obj[propertyName]}); 64 | 65 | return obj[propertyName] = React.createClass({ 66 | render: function(){ 67 | return React.DOM.div(); 68 | } 69 | }); 70 | }, 71 | 72 | addMethodToClass: function(klass, methodName, methodDefinition){ 73 | if(typeof methodDefinition === "undefined"){ 74 | methodDefinition = function(){}; 75 | } 76 | this.classPrototype(klass)[methodName] = methodDefinition; 77 | return klass; 78 | }, 79 | 80 | resetComponentStubs: function(){ 81 | for (var i = 0; i < componentStubs.length; i++) { 82 | var stub = componentStubs[i]; 83 | stub.obj[stub.propertyName] = stub.originalValue; 84 | } 85 | 86 | componentStubs = []; 87 | }, 88 | 89 | removeAllSpies: function(){ 90 | for (var i = 0; i < spies.length; i++) { 91 | var spy = spies[i]; 92 | if(spy.baseObj.__reactAutoBindMap){ 93 | spy.baseObj.__reactAutoBindMap[spy.methodName] = spy.originalValue; 94 | } 95 | spy.baseObj[spy.methodName] = spy.originalValue; 96 | } 97 | 98 | spies = []; 99 | }, 100 | 101 | unmountAllRenderedComponents: function(){ 102 | for (var i = 0; i < renderedComponents.length; i++) { 103 | var renderedComponent = renderedComponents[i]; 104 | this.unmountComponent(renderedComponent); 105 | } 106 | 107 | renderedComponents = []; 108 | }, 109 | 110 | unmountComponent: function(component){ 111 | if(component.isMounted()){ 112 | return React.unmountComponentAtNode(component.getDOMNode().parentNode); 113 | } else { 114 | return false; 115 | } 116 | }, 117 | 118 | getDefaultContainer: function(){ 119 | return document.getElementById("jasmine_content"); 120 | } 121 | }; 122 | 123 | // backwards compatability for React < 0.12 124 | jasmineReact.renderComponent = jasmineReact.render; 125 | 126 | // TODO: this has no automated test coverage. Add some integration tests for coverage. 127 | afterEach(function(){ 128 | jasmineReact.removeAllSpies(); 129 | jasmineReact.resetComponentStubs(); 130 | jasmineReact.unmountAllRenderedComponents(); 131 | }); 132 | 133 | module.exports = jasmineReact; 134 | -------------------------------------------------------------------------------- /test/documentation-spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The objective of this spec is to test all of the code samples in the official documentation. 3 | * If the code samples are wrong, people will think the library is wrong - which is no bueno. :) 4 | * Note: These tests might not be the best because they'll use global variables and stuff like that, 5 | * but for right now that trade-off is fine. That's another reason why this spec is completely 6 | * isolated from the core spec suite. 7 | */ 8 | 9 | describe("README.md", function(){ 10 | 11 | describe("Spying on a method in a React Class", function(){ 12 | window.HelloWorld = React.createClass({ 13 | getInitialState: function(){ 14 | return { number: this.randomNumber() }; 15 | }, 16 | randomNumber: function(){ 17 | return Math.random(); 18 | }, 19 | render: function() { 20 | return (
Hello {this.state.number}
); 21 | } 22 | }); 23 | 24 | describe("HelloWorld", function(){ 25 | it("can spy on a function for a React class", function(){ 26 | jasmineReact.spyOnClass(HelloWorld, "randomNumber").andReturn(42); 27 | 28 | // jasmineReact wraps React.render, so you don't have to worry 29 | // about the async nature of when the actual DOM get's rendered, or selecting 30 | // where your component needs to get rendered (default is #jasmine_content) 31 | var myWorld = jasmineReact.render(); 32 | 33 | expect(myWorld.state.number).toBe(42); 34 | }); 35 | 36 | it("can assert that a spy has been called", function(){ 37 | jasmineReact.spyOnClass(HelloWorld, "randomNumber"); 38 | 39 | jasmineReact.render(); 40 | 41 | // because we spy on the class and not the instance, we have to assert that the 42 | // function on the class' prototype was called. 43 | expect(jasmineReact.classPrototype(HelloWorld).randomNumber).toHaveBeenCalled(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("Replacing a component's subcomponent with a test double", function(){ 49 | window.Avatar = React.createClass({ 50 | render: function() { 51 | return ( 52 |
53 | 54 |
55 | ); 56 | } 57 | }); 58 | 59 | window.Profile = React.createClass({ 60 | render: function(){ 61 | throw("I like to blow up"); 62 | } 63 | }); 64 | 65 | describe("Avatar", function(){ 66 | 67 | it("should spy on a subcomponent and use a test double component", function(){ 68 | jasmineReact.createStubComponent(window, "Profile"); 69 | 70 | // This line won't throw the "I like to blow up" error because we've replaced the class with a test double! 71 | var avatar = jasmineReact.render(); 72 | 73 | expect(avatar.refs.pic.props.username).toBe("Zuck"); 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | 80 | describe("Adding a method to a component class", function(){ 81 | window.Avatar = React.createClass({ 82 | render: function() { 83 | return ( 84 |
85 | 86 |
87 | ); 88 | }, 89 | 90 | rotateProfile: function(){ 91 | this.refs.pic.rotate(); 92 | } 93 | }); 94 | 95 | describe("Avatar", function(){ 96 | describe("rotateProfile", function(){ 97 | it("should call 'rotate' on the Profile subcomponent", function(){ 98 | var profileClassStub = jasmineReact.createStubComponent(window, "Profile"); 99 | 100 | // We could also do: jasmineReact.addMethodToClass(window.Profile, "rotate", function(){}); 101 | jasmineReact.addMethodToClass(profileClassStub, "rotate", function(){}); 102 | jasmineReact.spyOnClass(profileClassStub, "rotate"); 103 | 104 | var avatar = jasmineReact.render(); 105 | 106 | expect(jasmineReact.classPrototype(profileClassStub).rotate).not.toHaveBeenCalled(); 107 | avatar.rotateProfile(); 108 | expect(jasmineReact.classPrototype(profileClassStub).rotate).toHaveBeenCalled(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /test/jasmine-react-spec.js: -------------------------------------------------------------------------------- 1 | describe("jasmineReact", function(){ 2 | 3 | describe("top level environment", function(){ 4 | it("should define one global object called 'jasmineReact'", function(){ 5 | expect(window.jasmineReact).toBeDefined(); 6 | }); 7 | }); 8 | 9 | describe("render", function(){ 10 | var FooKlass; 11 | 12 | beforeEach(function(){ 13 | FooKlass = React.createClass({ 14 | render: function(){ 15 | return React.DOM.div({}); 16 | } 17 | }); 18 | 19 | spyOn(React, "render").andCallThrough(); 20 | }); 21 | 22 | it("should call React.render with the passed in component", function(){ 23 | jasmineReact.render(, document.getElementById("jasmine_content")); 24 | 25 | var renderArgs = React.render.mostRecentCall.args[0]; 26 | 27 | expect(renderArgs.props.foo).toBe("bar"); 28 | }); 29 | 30 | it("should call React.render with the passed in container", function(){ 31 | var container = document.getElementById("jasmine_content"); 32 | jasmineReact.render(, container); 33 | 34 | expect(React.render).toHaveBeenCalledWith(jasmine.any(Object), container); 35 | }); 36 | 37 | it("should call React.render with #jasmine_content container if no container is passed in", function(){ 38 | jasmineReact.render(); 39 | 40 | expect(React.render).toHaveBeenCalledWith(jasmine.any(Object), document.getElementById("jasmine_content")); 41 | }); 42 | 43 | it("should call React.render with a callback if one is passed in", function(){ 44 | var fakeCallbackSpy = jasmine.createSpy("fakeCallback"); 45 | 46 | jasmineReact.render(, document.getElementById("jasmine_content"), fakeCallbackSpy); 47 | 48 | expect(React.render).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Object), fakeCallbackSpy); 49 | }); 50 | 51 | it("should return the return value of React.render", function(){ 52 | var returnValue = jasmineReact.render(, document.getElementById("jasmine_content")); 53 | 54 | expect(returnValue.props.baz).toBe("bat"); 55 | }); 56 | 57 | it("should alias jasmineReact.renderComponent to jasmineReact.render", function(){ 58 | var returnValue = jasmineReact.renderComponent(, document.getElementById("jasmine_content")); 59 | 60 | expect(returnValue.props.baz).toBe("bat"); 61 | }); 62 | }); 63 | 64 | describe("render: test pollution", function(){ 65 | it("should not pollute a rendered component from one test into another test", function(){ 66 | var CoolKlass = React.createClass({ 67 | render: function(){ 68 | return React.DOM.div({ 69 | id: "really-cool" 70 | }); 71 | } 72 | }); 73 | 74 | // lets pretend this is test #1 75 | jasmineReact.render(); 76 | 77 | expect(document.getElementById("really-cool")).toBeDefined(); 78 | 79 | // this is the method in the afterEach which is needed to prevent test pollution for render 80 | jasmineReact.unmountAllRenderedComponents(); 81 | 82 | // lets pretend this is test #1 83 | expect(document.getElementById("really-cool")).toBeNull(); 84 | }); 85 | }); 86 | 87 | describe("spyOnClass", function(){ 88 | var FooKlass; 89 | 90 | beforeEach(function(){ 91 | FooKlass = React.createClass({ 92 | render: function(){ 93 | return React.DOM.div({}); 94 | }, 95 | 96 | bar: function(){ 97 | return "real value"; 98 | } 99 | }); 100 | }); 101 | 102 | it("should allow a react class to have a function be spied on (when called externally)", function(){ 103 | jasmineReact.spyOnClass(FooKlass, "bar").andReturn("fake value"); 104 | 105 | var foo = jasmineReact.render(); 106 | 107 | expect(foo.bar()).not.toBe("real value"); 108 | expect(foo.bar()).toBe("fake value"); 109 | }); 110 | 111 | it("should allow a react class to have a function be spied on (when called internally in a lifecycle function)", function(){ 112 | var KlassWithAnInitialState = React.createClass({ 113 | render: function(){ 114 | return React.DOM.div({}); 115 | }, 116 | 117 | getInitialState: function(){ 118 | return { 119 | initialBar: this.bar() 120 | }; 121 | }, 122 | 123 | bar: function(){ 124 | return "real value"; 125 | } 126 | }); 127 | 128 | jasmineReact.spyOnClass(KlassWithAnInitialState, "bar").andReturn("fake value"); 129 | 130 | var foo = jasmineReact.render(); 131 | 132 | expect(foo.state.initialBar).not.toBe("real value"); 133 | expect(foo.state.initialBar).toBe("fake value"); 134 | }); 135 | 136 | it("should allow a react class to have a function be spied on (when called inside the render function)", function(){ 137 | var KlassWithARenderFunction = React.createClass({ 138 | render: function(){ 139 | return React.DOM.div({ 140 | className: this.bar() 141 | }); 142 | }, 143 | 144 | bar: function(){ 145 | return "real-value"; 146 | } 147 | }); 148 | 149 | jasmineReact.spyOnClass(KlassWithARenderFunction, "bar").andReturn("fake-value"); 150 | 151 | var foo = jasmineReact.render(); 152 | 153 | expect(foo.getDOMNode().className).not.toBe("real-value"); 154 | expect(foo.getDOMNode().className).toBe("fake-value"); 155 | }); 156 | 157 | it("should allow a react class to have a function which was added via 'jasmineReact.addMethodToClass' be spied on", function(){ 158 | var simpleKlass = React.createClass({ 159 | render: function(){ 160 | return React.DOM.div({}); 161 | } 162 | }); 163 | jasmineReact.addMethodToClass(simpleKlass, "fauxMethod", function(){}); 164 | jasmineReact.spyOnClass(simpleKlass, "fauxMethod").andCallFake(function(){}); 165 | }); 166 | 167 | it("should return the spy as the return value", function(){ 168 | var mySpy = jasmineReact.spyOnClass(FooKlass, "bar"); 169 | var foo = jasmineReact.render(); 170 | 171 | expect(mySpy.callCount).toBe(0); 172 | 173 | foo.bar(); 174 | 175 | expect(mySpy.callCount).toBe(1); 176 | }); 177 | 178 | it("should maintain regular jasmine spy behavior", function(){ 179 | jasmineReact.spyOnClass(FooKlass, "bar").andReturn(42); 180 | 181 | var foo = jasmineReact.render(); 182 | 183 | expect(foo.bar()).toBe(42); 184 | }); 185 | }); 186 | 187 | describe("spyOnClass: test pollution", function(){ 188 | it("should not pollute a spied on function from one test into another test", function(){ 189 | var BarKlass = React.createClass({ 190 | render: function(){ 191 | return React.DOM.div({}); 192 | }, 193 | 194 | bar: function(){ 195 | return "real value"; 196 | } 197 | }); 198 | 199 | // lets pretend this is test #1 200 | jasmineReact.spyOnClass(BarKlass, "bar").andCallFake(function(){ 201 | return "fake value"; 202 | }); 203 | var barOne = jasmineReact.render(); 204 | expect(barOne.bar()).toBe("fake value"); 205 | 206 | // these are the methods in the afterEach which are needed to prevent test pollution for spyOnClass 207 | jasmineReact.removeAllSpies(); 208 | jasmineReact.unmountAllRenderedComponents(); 209 | 210 | // lets pretend this is test #2 211 | var barTwo = jasmineReact.render(); 212 | expect(barTwo.bar()).toBe("real value"); 213 | }); 214 | }); 215 | 216 | describe("createStubComponent", function(){ 217 | var namespace; 218 | 219 | beforeEach(function(){ 220 | namespace = { 221 | Profile: "not a react class definition" 222 | }; 223 | }); 224 | 225 | it("should replace the property value with a valid react class definition", function(){ 226 | jasmineReact.createStubComponent(namespace, "Profile"); 227 | 228 | expect(jasmineReact.classPrototype(namespace.Profile).render).toBeDefined(); 229 | }); 230 | 231 | // React is now doing this itself ... 232 | // it("should have a react class definition which can be rendered", function(){ 233 | // jasmineReact.createStubComponent(namespace, "Profile"); 234 | // 235 | // expect(function(){ 236 | // jasmineReact.render(namespace.Profile()); 237 | // }).not.toThrow(); 238 | // }); 239 | 240 | it("should return the component stub", function(){ 241 | var returnValue = jasmineReact.createStubComponent(namespace, "Profile"); 242 | 243 | expect(returnValue).toBeDefined(); 244 | expect(jasmineReact.classPrototype(returnValue).render).toBeDefined(); 245 | }); 246 | }); 247 | 248 | describe("createStubComponent: test pollution", function(){ 249 | it("should reset the property value to the original value after the test", function(){ 250 | var namespace = { 251 | Profile: "not a react class definition" 252 | }; 253 | 254 | // lets pretend this is test #1 255 | expect(namespace.Profile).toBe("not a react class definition"); 256 | jasmineReact.createStubComponent(namespace, "Profile"); 257 | expect(namespace.Profile).not.toBe("not a react class definition"); 258 | expect(typeof namespace.Profile).toBe("function"); 259 | 260 | // React is now doing this itself ... 261 | // expect(function(){ 262 | // jasmineReact.render(namespace.Profile()); 263 | // }).not.toThrow(); 264 | 265 | // these are the methods in the afterEach which are needed to prevent test pollution for createStubComponent 266 | jasmineReact.resetComponentStubs(); 267 | 268 | // lets pretend this is test #2 269 | expect(namespace.Profile).toBe("not a react class definition"); 270 | }); 271 | }); 272 | 273 | describe("classPrototype", function(){ 274 | 275 | var FooKlass; 276 | 277 | beforeEach(function(){ 278 | FooKlass = React.createClass({ 279 | render: function(){ 280 | return React.DOM.div({}); 281 | }, 282 | 283 | bar: function(){}, 284 | baz: "test" 285 | }); 286 | }); 287 | 288 | it("should return the prototype of the react class' component constructor", function(){ 289 | var proto = jasmineReact.classPrototype(FooKlass); 290 | expect(proto.bar).toBeDefined(); 291 | expect(proto.baz).toBe("test"); 292 | }); 293 | 294 | it("should throw a friendly error if a component is passed in (instead of a component class definition)", function(){ 295 | var foo = jasmineReact.render(); 296 | 297 | expect(function(){ 298 | jasmineReact.classPrototype(foo); 299 | }).toThrow("A component constructor could not be found for this class. Are you sure you passed in a the component definition for a React component?"); 300 | }); 301 | }); 302 | 303 | describe("addMethodToClass", function(){ 304 | var FooKlass; 305 | 306 | beforeEach(function(){ 307 | FooKlass = React.createClass({ 308 | render: function(){ 309 | return React.DOM.div({}); 310 | } 311 | }); 312 | }); 313 | 314 | it("should allow a method to be added to a react component class", function(){ 315 | var fooOne = jasmineReact.render(); 316 | 317 | expect(fooOne.newMethod).toBeUndefined(); 318 | 319 | jasmineReact.addMethodToClass(FooKlass, "newMethod", function(){}); 320 | 321 | var fooTwo = jasmineReact.render(); 322 | 323 | expect(fooTwo.newMethod).toBeDefined(); 324 | }); 325 | 326 | it("should accept a method definition for the new method", function(){ 327 | jasmineReact.addMethodToClass(FooKlass, "newMethod", function(){ 328 | return "I'm a stub for a real method!"; 329 | }); 330 | 331 | var foo = jasmineReact.render(); 332 | 333 | expect(foo.newMethod()).toBe("I'm a stub for a real method!"); 334 | }); 335 | 336 | it("should default the method definition to a no-op", function(){ 337 | jasmineReact.addMethodToClass(FooKlass, "newMethod"); 338 | 339 | var foo = jasmineReact.render(); 340 | 341 | expect(foo.newMethod()).toBeUndefined(); 342 | }); 343 | 344 | it("should return the react class", function(){ 345 | var returnValue = jasmineReact.addMethodToClass(FooKlass, "newMethod", function(){}); 346 | 347 | expect(returnValue).toEqual(FooKlass); 348 | }); 349 | }); 350 | 351 | describe("unmountComponent", function(){ 352 | var componentWillUnmountSpy, BarKlass; 353 | 354 | beforeEach(function(){ 355 | componentWillUnmountSpy = jasmine.createSpy("componentWillUnmount"); 356 | 357 | BarKlass = React.createClass({ 358 | render: function(){ 359 | return React.DOM.div(); 360 | }, 361 | componentWillUnmount: function(){ 362 | componentWillUnmountSpy(); 363 | } 364 | }); 365 | 366 | }); 367 | 368 | describe("the component is mounted", function(){ 369 | it("should unmount the component", function(){ 370 | var barComponent = jasmineReact.render(); 371 | expect(componentWillUnmountSpy.callCount).toBe(0); 372 | 373 | jasmineReact.unmountComponent(barComponent); 374 | 375 | expect(componentWillUnmountSpy.callCount).toBe(1); 376 | }); 377 | 378 | it("should return the return value of unmountComponentAtNode", function(){ 379 | var barComponent = jasmineReact.render(); 380 | 381 | var returnValue = jasmineReact.unmountComponent(barComponent); 382 | 383 | expect(returnValue).toBe(true); 384 | }); 385 | }); 386 | 387 | describe("the component is not mounted", function(){ 388 | 389 | it("should not unmount the component", function(){ 390 | var barComponent = jasmineReact.render(); 391 | 392 | React.unmountComponentAtNode(barComponent.getDOMNode().parentNode); 393 | 394 | expect(componentWillUnmountSpy.callCount).toBe(1); 395 | 396 | expect(function(){ 397 | jasmineReact.unmountComponent(barComponent); 398 | }).not.toThrow(); 399 | 400 | expect(componentWillUnmountSpy.callCount).toBe(1); 401 | }); 402 | 403 | it("should return false", function(){ 404 | var barComponent = jasmineReact.render(); 405 | 406 | React.unmountComponentAtNode(barComponent.getDOMNode().parentNode); 407 | 408 | var returnValue; 409 | 410 | expect(function(){ 411 | returnValue = jasmineReact.unmountComponent(barComponent); 412 | }).not.toThrow(); 413 | 414 | expect(returnValue).toBe(false); 415 | }); 416 | }); 417 | 418 | 419 | }); 420 | 421 | }); 422 | -------------------------------------------------------------------------------- /test/support/jasmine-content.js: -------------------------------------------------------------------------------- 1 | // add div#jasmine_content since it is required by jasmineReact 2 | var content = document.createElement('div'); 3 | content.id = 'jasmine_content'; 4 | document.body.appendChild(content); -------------------------------------------------------------------------------- /test/support/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | // Polyfill Function.prototype.bind for PhantomJS 2 | // see https://github.com/facebook/react/pull/347 3 | // source: https://github.com/facebook/react/blob/master/src/test/phantomjs-shims.js 4 | 5 | (function() { 6 | 7 | var Ap = Array.prototype; 8 | var slice = Ap.slice; 9 | var Fp = Function.prototype; 10 | 11 | if (!Fp.bind) { 12 | // PhantomJS doesn't support Function.prototype.bind natively, so 13 | // polyfill it whenever this module is required. 14 | Fp.bind = function(context) { 15 | var func = this; 16 | var args = slice.call(arguments, 1); 17 | 18 | function bound() { 19 | var invokedAsConstructor = func.prototype && (this instanceof func); 20 | return func.apply( 21 | // Ignore the context parameter when invoking the bound function 22 | // as a constructor. Note that this includes not only constructor 23 | // invocations using the new keyword but also calls to base class 24 | // constructors such as BaseClass.call(this, ...) or super(...). 25 | !invokedAsConstructor && context || this, 26 | args.concat(slice.call(arguments)) 27 | ); 28 | } 29 | 30 | // The bound function must share the .prototype of the unbound 31 | // function so that any object created by one constructor will count 32 | // as an instance of both constructors. 33 | bound.prototype = func.prototype; 34 | 35 | return bound; 36 | }; 37 | } 38 | 39 | })(); -------------------------------------------------------------------------------- /test/support/react.js: -------------------------------------------------------------------------------- 1 | // TODO: replace both of these with require statements in the spec files 2 | window.React = require('react/addons'); 3 | window.jasmineReact = require("../../src/jasmine-react"); --------------------------------------------------------------------------------