├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── circle.yml
├── index.js
├── package.json
├── src
└── index.js
├── tests
├── components
│ ├── address-line
│ │ ├── address-line-styles.js
│ │ ├── address-line-tests.js
│ │ └── address-line.jsx
│ ├── address
│ │ ├── address-tests.js
│ │ └── address.jsx
│ ├── app
│ │ ├── app-styles.js
│ │ ├── app-tests.js
│ │ └── app.jsx
│ ├── message
│ │ ├── message-styles.js
│ │ ├── message-tests.js
│ │ └── message.jsx
│ ├── postcard
│ │ ├── postcard-styles.js
│ │ ├── postcard-tests.js
│ │ └── postcard.jsx
│ ├── signature
│ │ ├── signature-styles.js
│ │ ├── signature-tests.js
│ │ └── signature.jsx
│ ├── stamp
│ │ ├── stamp-dumb-tests.js
│ │ ├── stamp-dumb.jsx
│ │ ├── stamp-smart-tests.js
│ │ ├── stamp-smart.jsx
│ │ ├── stamp-styles.js
│ │ └── stamp-types.js
│ └── welcome
│ │ ├── welcome-styles.js
│ │ ├── welcome-tests.js
│ │ └── welcome.jsx
├── index.html
├── index.jsx
├── stand-alone
│ ├── bad-components
│ │ ├── multi-root
│ │ │ ├── item-list-tests.js
│ │ │ ├── item-list.jsx
│ │ │ └── item.jsx
│ │ └── some-error
│ │ │ ├── some-error-tests.js
│ │ │ └── some-error.js
│ ├── link-list
│ │ ├── link-list-tests.js
│ │ ├── link-list.jsx
│ │ └── link.jsx
│ ├── map-to
│ │ ├── on-change-tests.js
│ │ └── on-change.jsx
│ ├── null-component
│ │ ├── null-component-tests.js
│ │ └── null-component.js
│ ├── spies-config
│ │ ├── spies-config-tests.js
│ │ └── spies-config.jsx
│ ├── spies-teardown
│ │ ├── spies-teardown-tests.js
│ │ └── spies-teardown.jsx
│ └── unordered-list
│ │ ├── unordered-list-tests.jsx
│ │ └── unordered-list.jsx
└── webpack.config.test.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.min.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "func-names": 0,
5 | "prefer-rest-params": 0,
6 | "no-underscore-dangle": 0,
7 | "import/no-extraneous-dependencies": 0
8 | },
9 | "globals": {
10 | "describe": true,
11 | "it": true,
12 | "beforeEach": true,
13 | "afterEach": true,
14 | }
15 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | tests/index.min*
4 | npm-debug.log
5 | coverage
6 | .DS_Store
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # We're going through changes
2 |
3 | ## 0.1.0 - 28 January, 2016
4 |
5 | * Add `withArgs` to help with `propFunc` `mapsTo`
6 | * Add defensive spy code and tidied methods to spy on
7 |
8 | ## 0.0.23 - 12 January, 2016
9 |
10 | * Fake context when using radium (after v0.16 breaking changes)
11 |
12 | ## 0.0.22 - 24 December, 2015
13 |
14 | * Upgrade to babel 6
15 |
16 | ## 0.0.21 - 12 December, 2015
17 |
18 | * Fix child event mapping
19 |
20 | ## 0.0.20 - 12 December, 2015
21 |
22 | * Fix unspyable methods
23 | * Throw error if more than one root element
24 |
25 | ## 0.0.19 - 9 December, 2015
26 |
27 | * Add spy config
28 |
29 | ## 0.0.18 - 12 November, 2015
30 |
31 | * Add teardown functionality
32 |
33 | ## 0.0.16 - 11 November, 2015
34 |
35 | * Allow for render methods that return null!?
36 |
37 | ## 0.0.15 - 11 November, 2015
38 |
39 | * Add event mapping for child elements
40 |
41 | ## 0.0.14 - 11 November, 2015
42 |
43 | * Fix event mapping for multiple events in component
44 |
45 | ## 0.0.12 - 5 November, 2015
46 |
47 | * Fix deep child nesting
48 |
49 | ## 0.0.9 - 5 November, 2015
50 |
51 | * Fix value property for elements with no children
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/craigbilner/react-component-tester/tree/master)
2 | [](https://coveralls.io/github/craigbilner/react-component-tester?branch=master)
3 | 
4 | [](https://www.npmjs.com/package/react-component-tester)
5 | [](https://www.npmjs.com/package/react-component-tester)
6 |
7 | # react component tester
8 |
9 | because testing components with shallow rendering is fantastic...but can be a bit of a pain and other testing libraries focus on finding components rather than providing an opinionated way of testing them, including their state.
10 |
11 | # requirements
12 |
13 | * test a component quickly (~5ms per test)
14 | * test the style of components easily
15 | * test the props of child components easily
16 | * test various "flavours" of props succinctly in a consistent uniform manner
17 | * find child components easily in a non-brittle manner
18 | * test prop wire-up on smart components
19 | * test changes of state of smart components
20 | * abstract away from the react components themselves and their API
21 |
22 | # usage
23 |
24 | a component system that builds a basic responsive postcard can be found in the tests directory with example usage (which also self tests the library)
25 |
26 | ## flavours
27 |
28 | often you want to test how components are rendered with different prop values. this tester allows the addition of "flavours" to make the test code more terse
29 |
30 | to create a tester, run create and pass in the component under test to "be used"
31 |
32 | ```javascript
33 | const tester = ReactTester.create().use(Address);
34 | ```
35 |
36 | then add a flavour which takes a name and a props object
37 |
38 | ```javascript
39 | const NO_ADDRESS = tester.addFlavour('NO_ADDRESS', {
40 | addressee: 'Mr Robert Smith',
41 | });
42 | ```
43 |
44 | then the flavour can be tested on, here the NONE flavour's type is checked
45 |
46 | ```javascript
47 | const actual = NONE.type;
48 | const expected = 'div';
49 |
50 | assert.deepEqual(actual, expected);
51 | ```
52 |
53 | ## methods
54 |
55 | ### .countComponents(Component{ReactComponent}) => count{int}
56 |
57 | takes a react component and returns the number of times it appears in the rendered tree for the component under test
58 |
59 | here the THREE_LINES flavour is tested for the number of AddressLine components it would render if given three lines of address
60 |
61 | ```javascript
62 | it('render four address lines if there are three lines in the given address', () => {
63 | const actual = THREE_LINES.countComponents(AddressLine);
64 | const expected = 4;
65 |
66 | assert.deepEqual(actual, expected);
67 | });
68 | ```
69 |
70 | ### .findChild(path{string}) => child{ReactComponent}
71 |
72 | takes a zero based decimal delimited string path and returns the resulting rendered component. this will make your tests more brittle to DOM changes but can come in handy
73 |
74 | here the first child is found. if we wanted the second child of the third child of the fourth child we would pass "3.2.1"
75 |
76 | ```javascript
77 | const value = NOTHING.findChild('0');
78 | const actual = typeof value;
79 | const expected = 'undefined';
80 |
81 | assert.deepEqual(actual, expected);
82 | ```
83 |
84 | ### .findComponents(Component{ReactComponent}) => [Component{ReactComponent}]
85 |
86 | takes a react component and returns an array of all child rendered components of that type
87 |
88 | here we find all AddressLine components for the NO_ADDRESS flavour, take the first one and check that it is the addressee
89 |
90 | ```javascript
91 | const addressee = NO_ADDRESS.findComponents(AddressLine)[0];
92 | const actual = addressee.props.text;
93 | const expected = 'Mr Robert Smith';
94 |
95 | assert.deepEqual(actual, expected);
96 | ```
97 |
98 | ### .getState() => state{object}
99 |
100 | returns the current state of the given component flavour. resetState should be run after each test to ensure tests do not collide
101 |
102 | here we fire the click function given to the dumb component and confirm that the smart component's state is updated correctly
103 |
104 | ```javascript
105 | NONE.component.props.onClick();
106 |
107 | const actual = NONE.getState().type;
108 | const expected = stampTypes.FIRST_CLASS;
109 |
110 | assert.deepEqual(actual, expected);
111 | ```
112 |
113 | ### .propFunc(propName{string}), .withArgs(arguments{array}) [optional], .mapsTo(methodName{string}) => isMapped{boolean}
114 |
115 | `propFunc` takes a string which is the prop of a dumb component to which you are passing a function
116 |
117 | `mapsTo` takes a string which maps to the method on the smart component class
118 |
119 | if your `mapsTo` method expects some specific arguments, these can be passed as an array using `withArgs`
120 |
121 | the returned value will be a boolean indicating if the given prop function maps to the expected class method
122 |
123 | here we test that the stamp dumb component was correctly given the smart component's `handleOnClick` method
124 |
125 | ```javascript
126 | const isMapped =
127 | NONE
128 | .component
129 | .propFunc('onClick')
130 | .withArgs({ test: 123 })
131 | .mapsTo('handleOnClick');
132 |
133 | assert(isMapped);
134 | ```
135 |
136 | ### .resetState()
137 |
138 | returns the state of the flavour of the component under test to the initial state
139 |
140 | ### .teardown() => {ReactComponentTester}
141 |
142 | If for any reason you need to restore spies placed on a component as part of set up you can use the teardown method. Returns the tester.
143 |
144 | ```javascript
145 | const tester = ReactTester.create().use(Address);
146 | // Do stuff...
147 | tester.teardown();
148 | ```
149 |
150 | ## properties
151 |
152 | ### component{renderedReactComponent}
153 |
154 | the rendered component that is under test
155 |
156 | ### props{object}
157 |
158 | the props of the rendered component will be returned for each component found
159 |
160 | ### style{object}
161 |
162 | for ease of use, the style prop is placed on each component too
163 |
164 | ### type{componentType}
165 |
166 | each returned component will have a type
167 |
168 | ### value{string}
169 |
170 | if a component doesn't have any children, it's content will be added as the value property
171 |
172 | ## config
173 |
174 | all methods are spied on by default, if for some reason you would like to remove them you can pass a config on create
175 |
176 | ```javascript
177 | ReactTester.create({
178 | spyOnDefault: false,
179 | spyOn: {
180 | someMethod: false,
181 | },
182 | }).use(SomeComponent);
183 | ```
184 |
185 | You can either pass:
186 |
187 | * `spyOnDefault` as false to disable all spies
188 | * a `spyOn` config to disable per method
189 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | cache_directories:
3 | - ~/.cache/yarn
4 | override:
5 | - yarn
6 | machine:
7 | environment:
8 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
9 | node:
10 | version: "6.10.0"
11 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const ReactTester = require('./src/index');
2 |
3 | module.exports = ReactTester;
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-component-tester",
3 | "version": "0.4.0",
4 | "description": "A simple helper to make testing react components with shallow rendering easier",
5 | "main": "index.js",
6 | "scripts": {
7 | "build:test": "webpack --config ./tests/webpack.config.test.js",
8 | "build:testrig": "npm run build:test && open ./tests/index.html",
9 | "watch:testrig": "npm run build:test -- --progress --colors --watch",
10 | "test": "babel-node ./node_modules/istanbul/lib/cli cover _mocha ./tests/**/*tests.js",
11 | "posttest": "npm run lint && cat ./coverage/lcov.info | coveralls",
12 | "test:simple": "mocha ./tests/**/*tests.js --compilers js:babel-core/register",
13 | "test:spec": "mocha ./tests/components/address-line --compilers js:babel-core/register",
14 | "lint": "eslint ./src ./tests ./index.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/craigbilner/react-component-tester.git"
19 | },
20 | "keywords": [
21 | "react",
22 | "testing",
23 | "shallow",
24 | "rendering",
25 | "react-test",
26 | "react-testing",
27 | "react-tester",
28 | "react-component",
29 | "react-shallow-rendering",
30 | "react-test-help",
31 | "react-testing-helpers"
32 | ],
33 | "author": "Craig Bilner",
34 | "license": "Apache-2.0",
35 | "bugs": {
36 | "url": "https://github.com/craigbilner/react-component-tester/issues"
37 | },
38 | "homepage": "https://github.com/craigbilner/react-component-tester#readme",
39 | "devDependencies": {
40 | "babel-cli": "6.14.0",
41 | "babel-core": "6.14.0",
42 | "babel-loader": "6.4.1",
43 | "babel-preset-es2015": "6.14.0",
44 | "babel-preset-react": "6.23.0",
45 | "coveralls": "2.11.16",
46 | "eslint": "3.18.0",
47 | "eslint-config-airbnb": "14.1.0",
48 | "eslint-plugin-import": "2.2.0",
49 | "eslint-plugin-jsx-a11y": "4.0.0",
50 | "eslint-plugin-react": "6.10.3",
51 | "istanbul": "0.4.5",
52 | "mocha": "3.2.0",
53 | "radium": "0.18.2",
54 | "react-dom": "15.3.1",
55 | "webpack": "1.13.2"
56 | },
57 | "dependencies": {
58 | "lodash": "4.17.4",
59 | "react": "15.3.1",
60 | "react-addons-test-utils": "15.3.1",
61 | "sinon": "2.1.0",
62 | "stampit": "3.1.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const stampit = require('stampit');
2 | const _ = require('lodash'); // eslint-disable-line id-length
3 | const React = require('react');
4 | const TestUtils = require('react-addons-test-utils');
5 | const sinon = require('sinon');
6 |
7 | const defaultConfig = {
8 | spyOnDefault: true,
9 | spyOn: {},
10 | };
11 |
12 | const doNotSpyOn = {
13 | constructor: false,
14 | componentWillUnmount: false,
15 | render: false,
16 | getChildContext: false,
17 | getNextStampType: false,
18 | };
19 |
20 | // FLAVOUR COMPONENT
21 |
22 | // METHODS
23 |
24 | const propFunc = function (propToTest) {
25 | this.propToTest = propToTest;
26 | return this;
27 | };
28 |
29 | const withArgs = function () {
30 | this.mapTestArgs = [].slice.call(arguments);
31 | return this;
32 | };
33 |
34 | const mapsTo = function (method) {
35 | const symbolToTest = Symbol(method);
36 |
37 | this.reactClass.prototype[method].reset();
38 | this.props[this.propToTest].apply(this.reactClass, this.mapTestArgs.concat(symbolToTest));
39 |
40 | return this.reactClass.prototype[method].lastCall.args.indexOf(symbolToTest) > -1;
41 | };
42 |
43 | const flavourComponentMethods = {
44 | propFunc,
45 | withArgs,
46 | mapsTo,
47 | };
48 |
49 | const flavourComponentInit = function (opts) {
50 | Object.assign(this, opts);
51 |
52 | this.mapTestArgs = [];
53 |
54 | if (opts.props) {
55 | this.style = opts.props.style;
56 |
57 | if (!TestUtils.isElement(opts.props.children)) {
58 | this.value = opts.props.children;
59 | }
60 | }
61 | };
62 |
63 | const flavourComponent = stampit()
64 | .init(flavourComponentInit)
65 | .methods(flavourComponentMethods);
66 |
67 | // FLAVOUR
68 |
69 | // METHODS
70 |
71 | /* eslint-disable no-use-before-define */
72 | const reduceChildren =
73 | function (parentComponent, reactClass, parentIndx, childMap, child, indx) {
74 | const thisMap = _.assign({}, childMap);
75 | const childIsElement = TestUtils.isElement(child);
76 | const id = parseInt(parentIndx, 10) >= 0 ? `${parentIndx}.${indx}` : indx;
77 |
78 | if (childIsElement) {
79 | thisMap[id] = flavourComponent(_.assign(
80 | {},
81 | child,
82 | {
83 | parentComponent,
84 | reactClass,
85 | }));
86 |
87 | _.assign(
88 | thisMap,
89 | convertAndReduce(
90 | parentComponent,
91 | reactClass,
92 | child.props.children,
93 | id));
94 | } else {
95 | thisMap[id] = {
96 | value: child,
97 | };
98 | }
99 |
100 | return thisMap;
101 | };
102 | /* eslint-enable no-use-before-define */
103 |
104 | const convertAndReduce = function (parentComponent, reactClass, children, parentIndx) {
105 | return React
106 | .Children
107 | .toArray(children)
108 | .reduce(
109 | reduceChildren.bind(null, parentComponent, reactClass, parentIndx),
110 | {});
111 | };
112 |
113 | const reduceTypes = function (childMap, typeMap, key) {
114 | const components = (typeMap.get(childMap[key].type) || []).slice(0);
115 | components.push(childMap[key]);
116 |
117 | typeMap.set(childMap[key].type, components);
118 |
119 | return typeMap;
120 | };
121 |
122 | const flavourInit = function (opts) {
123 | Object.assign(this, opts);
124 |
125 | const output = opts.shallowRenderer.getRenderOutput();
126 |
127 | this.initialState = _.assign({}, opts.shallowRenderer._instance._instance.state);
128 | this.state = _.assign({}, this.initialState);
129 | this.component = flavourComponent(_.assign(
130 | {},
131 | output,
132 | {
133 | reactClass: opts.reactClass,
134 | }));
135 |
136 | if (output) {
137 | this.type = output.type;
138 | this.childMap = convertAndReduce(
139 | this.component,
140 | opts.reactClass,
141 | output.props.children);
142 | } else {
143 | this.type = output;
144 | this.childMap = [];
145 | }
146 |
147 | this.typeMap =
148 | Object
149 | .keys(this.childMap)
150 | .reduce(
151 | reduceTypes.bind(null, this.childMap),
152 | new Map());
153 |
154 | // console.log('childMap', this.childMap);
155 | // console.log('typeMap', this.typeMap);
156 | };
157 |
158 | const resetState = function () {
159 | this.shallowRenderer._instance._instance.state = _.assign({}, this.initialState);
160 | };
161 |
162 | const getState = function () {
163 | return this.shallowRenderer._instance._instance.state;
164 | };
165 |
166 | const findChild = function (path) {
167 | return this.childMap[path];
168 | };
169 |
170 | const findComponents = function (component) {
171 | return this.typeMap.get(component) || [];
172 | };
173 |
174 | const countComponents = function (component) {
175 | return this.findComponents(component).length;
176 | };
177 |
178 | const flavourMethods = {
179 | getState,
180 | resetState,
181 | findChild,
182 | findComponents,
183 | countComponents,
184 | };
185 |
186 | const flavour = stampit()
187 | .init(flavourInit)
188 | .methods(flavourMethods);
189 |
190 | // TESTER
191 |
192 | // INIT
193 | const testerInit = function (opts) {
194 | Object.assign(this, opts);
195 |
196 | this.ComponentToUse = null;
197 |
198 | this.config = {
199 | spyOnDefault: opts.spyOnDefault,
200 | spyOn: opts.spyOn,
201 | };
202 | };
203 |
204 | // METHODS
205 | const restoreSpy = function (method) {
206 | if (method.isSinonProxy) {
207 | method.restore();
208 | }
209 | };
210 |
211 | const use = function (Component) {
212 | const options = _.merge(
213 | {},
214 | defaultConfig,
215 | this.config,
216 | {
217 | spyOn: doNotSpyOn,
218 | });
219 |
220 | Object.getOwnPropertyNames(Component.prototype).forEach((method) => {
221 | if (options.spyOn[method] === false) return;
222 |
223 | if (options.spyOnDefault || options.spyOn[method]) {
224 | restoreSpy(Component.prototype[method]);
225 | sinon.spy(Component.prototype, method);
226 | }
227 | });
228 |
229 | this.ComponentToUse = Component;
230 |
231 | return this;
232 | };
233 |
234 | const createFlavour = function (name, reactClass, shallowRenderer) {
235 | return flavour({
236 | name,
237 | reactClass,
238 | shallowRenderer,
239 | });
240 | };
241 |
242 | const getShallowRenderer = function (component, props) {
243 | const shallowRenderer = TestUtils.createRenderer();
244 | const context = {};
245 |
246 | const componentWithProps = React.createElement(component, props);
247 |
248 | if (componentWithProps.type.contextTypes && componentWithProps.type.contextTypes._radiumConfig) {
249 | context._radiumConfig = function () {
250 | };
251 | context._radiumConfig.plugins = [];
252 |
253 | componentWithProps.type.contextTypes._radiumConfig = context._radiumConfig;
254 | }
255 |
256 | shallowRenderer
257 | .render(componentWithProps, context);
258 |
259 | return shallowRenderer;
260 | };
261 |
262 | const addFlavour = function (name, props) {
263 | return createFlavour(
264 | name,
265 | this.ComponentToUse,
266 | getShallowRenderer(this.ComponentToUse, props));
267 | };
268 |
269 | const teardown = function () {
270 | const proto = this.ComponentToUse.prototype;
271 |
272 | Object.getOwnPropertyNames(proto).forEach((method) => {
273 | restoreSpy(proto[method]);
274 | });
275 |
276 | return this;
277 | };
278 |
279 | const testerMethods = {
280 | use,
281 | addFlavour,
282 | teardown,
283 | };
284 |
285 | // STAMP
286 | const ReactTester =
287 | stampit()
288 | .init(testerInit)
289 | .methods(testerMethods);
290 |
291 | module.exports = ReactTester;
292 |
--------------------------------------------------------------------------------
/tests/components/address-line/address-line-styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | comp: {
3 | fontSize: '1.25rem',
4 | padding: '0.5rem 0 0.25rem 0',
5 | borderBottom: '1px solid black',
6 | boxSizing: 'border-box',
7 | textAlign: 'right',
8 | '@media (min-width: 1200px)': {
9 | fontSize: '2.5rem',
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/tests/components/address-line/address-line-tests.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import AddressLine from './address-line';
3 | import ReactTester from '../../../src/index';
4 |
5 | const tester = ReactTester.create().use(AddressLine);
6 | const NOTHING = tester.addFlavour('NOTHING', {});
7 | const SOMETHING = tester.addFlavour('SOMETHING', {
8 | text: 'some line of the address',
9 | });
10 |
11 | describe('line should', () => {
12 | it('render nothing if no line is given', () => {
13 | const value = NOTHING.findChild('0');
14 | const actual = typeof value;
15 | const expected = 'undefined';
16 |
17 | assert.deepEqual(actual, expected);
18 | });
19 |
20 | it('render the given text', () => {
21 | const actual = SOMETHING.findChild('0').value;
22 | const expected = 'some line of the address';
23 |
24 | assert.deepEqual(actual, expected);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/components/address-line/address-line.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import radium from 'radium';
3 | import styles from './address-line-styles';
4 |
5 | export default radium(({ text }) => (
6 |
7 | {text}
8 |
9 | ));
10 |
--------------------------------------------------------------------------------
/tests/components/address/address-tests.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Address from './address';
3 | import AddressLine from '../address-line/address-line';
4 | import ReactTester from '../../../src/index';
5 |
6 | const tester = ReactTester.create().use(Address);
7 | const NO_ADDRESS = tester.addFlavour('NO_ADDRESS', {
8 | addressee: 'Mr Robert Smith',
9 | });
10 | const THREE_LINES = tester.addFlavour('THREE_LINES', {
11 | addressee: 'Mr Joanne Robinson',
12 | address: ['line 31', 'line 32', 'line 33'],
13 | });
14 | const FIVE_LINES = tester.addFlavour('FIVE_LINES', {
15 | addressee: 'Mrs Jack Jones',
16 | address: ['line 51', 'line 52', 'line 53', 'Line 54', 'Line 55'],
17 | });
18 |
19 | describe('address should', () => {
20 | it('render the addressee as the first line if there is no address', () => {
21 | const addressee = NO_ADDRESS.findComponents(AddressLine)[0];
22 | const actual = addressee.props.text;
23 | const expected = 'Mr Robert Smith';
24 |
25 | assert.deepEqual(actual, expected);
26 | });
27 |
28 | it('render the addressee as the first line if there is an address', () => {
29 | const addressee = THREE_LINES.findComponents(AddressLine)[0];
30 | const actual = addressee.props.text;
31 | const expected = 'Mr Joanne Robinson';
32 |
33 | assert.deepEqual(actual, expected);
34 | });
35 |
36 | it('render four address lines if there are three lines in the given address', () => {
37 | const actual = THREE_LINES.countComponents(AddressLine);
38 | const expected = 4;
39 |
40 | assert.deepEqual(actual, expected);
41 | });
42 |
43 | it('render six address lines if there are five lines in the given address', () => {
44 | const actual = FIVE_LINES.countComponents(AddressLine);
45 | const expected = 6;
46 |
47 | assert.deepEqual(actual, expected);
48 | });
49 |
50 | it('render the expected first line of the address if three lines are given', () => {
51 | const actual = THREE_LINES.findComponents(AddressLine)[1].props.text;
52 | const expected = 'line 31';
53 |
54 | assert.deepEqual(actual, expected);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/tests/components/address/address.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import AddressLine from '../address-line/address-line';
3 |
4 | const Address = ({ address = [], addressee = '' }) => (
5 |