-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Add addon jest #2295
Add addon jest #2295
Changes from 30 commits
f6df222
1db27cb
b1db164
1433160
812c054
f1f869c
e66e375
3af0b87
f38a027
37d3748
b4c0d31
758c195
d004bd7
3c8c783
1073760
d185d1a
cc1eef2
2d17ece
b42538e
e310ec2
3320ee3
6648656
663d5ae
ab0d84d
d0d0494
045bca1
24b1982
1f2edf0
6dbbb34
793b76e
80bc824
d3ceba4
59fff75
7d95fae
eb33a3e
7206c93
9d83982
70d3424
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Storybook addon Jest | ||
|
||
Brings Jest results in storybook. | ||
|
||
[](https://storybook-addon-jest-example.herokuapp.com/) | ||
|
||
> Checkout the above [Live Storybook](https://storybook-addon-jest-example.herokuapp.com/). | ||
|
||
## Getting started | ||
|
||
### Install | ||
|
||
`npm install --save-dev @storybook/addon-jest` | ||
|
||
or | ||
|
||
`yarn add --dev @storybook/addon-jest` | ||
|
||
### Jest Configuration | ||
|
||
When running **Jest**, be sure to save the results in a json file: | ||
|
||
`package.json` | ||
|
||
```js | ||
"scripts": { | ||
"test": "jest --json --outputFile=.jest-test-results.json" | ||
} | ||
``` | ||
|
||
Add it the result file to `.gitignore`: | ||
|
||
``` | ||
.jest-test-results.json | ||
``` | ||
|
||
## Generating the test results | ||
|
||
You should run jest before you start storybook and have the json file generated prior. | ||
During development you will likely start jest in watch-mode | ||
and so the json file will be re-generated every time code or tests change. | ||
|
||
```sh | ||
npm run test:generate-output -- --watch | ||
``` | ||
|
||
This change will then be HMR (hot module reloaded) using webpack and displayed by this addon. | ||
|
||
If you want to pre-run jest automaticly during development or a static build, | ||
you may need to consider that if your tests fail, the script receives a non-0 exit code and will exit. | ||
You could create a `prebuild:storybook` npm script, which will never fail by appending `|| true`: | ||
```json | ||
"scripts": { | ||
"test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", | ||
"test": "jest", | ||
"prebuild:storybook": "npm run test:generate-output", | ||
"build:storybook": "build-storybook -c .storybook -o build/", | ||
"predeploy": "npm run build:storybook", | ||
"deploy": "gh-pages -d build/", | ||
} | ||
``` | ||
|
||
### Register | ||
|
||
Register addon at `.storybook/addons.js` | ||
|
||
```js | ||
import '@storybook/addon-jest/register'; | ||
``` | ||
|
||
## Usage | ||
|
||
Assuming that you have created a test files `MyComponent.test.js` and `MyOtherComponent.test.js` | ||
|
||
In your `story.js` | ||
|
||
```js | ||
import jestTestResults from '../.jest-test-results.json'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does that mean that everybody has to run the tests locally before ever starting storybook? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, edited the readme to make this clear. For demo purposes I've added the testresults-file to source control. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What exactly does it demonstrate? Users are not supposed to create those files manually, are they? Or do you mean "to make building our demos possible"? |
||
import withTests from '@storybook/addon-jest'; | ||
|
||
storiesOf('MyComponent', module) | ||
.addDecorator(withTests(jestTestResults, { filesExt: '.test.js' })('MyComponent', 'MyOtherComponent')); | ||
``` | ||
|
||
Or in order to avoid importing `.jest-test-results.json` in each story, you can create a simple file `withTests.js`: | ||
|
||
```js | ||
import jestTestResults from '../.jest-test-results.json'; | ||
import withTests from '@storybook/addon-jest'; | ||
|
||
export default withTests(jestTestResults, { | ||
filesExt: '.test.js', | ||
}); | ||
``` | ||
|
||
Then in your story: | ||
|
||
```js | ||
// import your file | ||
import withTests from '.withTests'; | ||
|
||
storiesOf('MyComponent', module) | ||
.addDecorator(withTests('MyComponent', 'MyOtherComponent')); | ||
``` | ||
|
||
### Styling | ||
|
||
The panel comes with a basic design. If you want to make it look a bit nicer, you add github markdown style by importing it in `.storybook/addons.js` | ||
|
||
```js | ||
import '@storybook/addon-jest/register'; | ||
import '@storybook/addon-jest/styles'; | ||
``` | ||
|
||
## TODO | ||
|
||
- [ ] Add coverage | ||
- [ ] Display nested test better (describe) | ||
- [ ] Display the date of the test | ||
- [ ] Add unit tests | ||
- [ ] Add linting | ||
- [ ] Split <TestPanel /> | ||
|
||
## Contributing | ||
|
||
Every ideas and contributions are welcomed. | ||
|
||
## Licence | ||
|
||
MIT © 2017-present Renaud Tertrais |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"name": "@storybook/addon-jest", | ||
"version": "3.2.15", | ||
"description": "React storybook addon that show component jest report", | ||
"keywords": [ | ||
"addon", | ||
"jest", | ||
"react", | ||
"report", | ||
"results", | ||
"storybook", | ||
"unit-testing" | ||
], | ||
"homepage": "https://storybook.js.org", | ||
"bugs": "https://github.com/storybooks/storybook", | ||
"license": "MIT", | ||
"author": "Renaud Tertrais <[email protected]> (https://github.com/renaudtertrais)", | ||
"main": "dist/index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/storybooks/storybook" | ||
}, | ||
"scripts": { | ||
"prepare": "node ../../scripts/prepare.js" | ||
}, | ||
"dependencies": { | ||
"@storybook/addons": "^3.2.15", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make it a peerDep from day one |
||
"@storybook/components": "^3.2.15", | ||
"global": "^4.3.2", | ||
"prop-types": "^15.6.0", | ||
"glamor": "^2.20.40", | ||
"glamorous": "^4.11.0" | ||
}, | ||
"devDependencies": { | ||
"react": "^16.1.0", | ||
"react-dom": "^16.1.0" | ||
}, | ||
"peerDependencies": { | ||
"react": "*", | ||
"react-dom": "*" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require('./dist/register'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default { | ||
success: 'LIGHTSEAGREEN', | ||
error: 'CRIMSON', | ||
warning: 'DARKORANGE', | ||
grey: 'LIGHTSLATEGRAY', | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
import glamorous from 'glamorous'; | ||
|
||
const Indicator = glamorous.div( | ||
({ color, size }) => ({ | ||
boxSizing: 'border-box', | ||
padding: `0 ${size / 2}px`, | ||
minWidth: size, | ||
minHeight: size, | ||
fontSize: size / 1.4, | ||
lineHeight: `${size}px`, | ||
color: 'white', | ||
textTransform: 'uppercase', | ||
borderRadius: size / 2, | ||
backgroundColor: color, | ||
}), | ||
({ styles }) => ({ | ||
...styles, | ||
}) | ||
); | ||
|
||
Indicator.defaultProps = { | ||
right: false, | ||
children: '', | ||
}; | ||
|
||
Indicator.propTypes = { | ||
color: PropTypes.string.isRequired, | ||
size: PropTypes.number.isRequired, | ||
children: PropTypes.node, | ||
right: PropTypes.bool, | ||
}; | ||
|
||
export default Indicator; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import glamorous from 'glamorous'; | ||
|
||
import { baseFonts } from '@storybook/components'; | ||
|
||
import Indicator from './Indicator'; | ||
import Result, { FailedResult } from './Result'; | ||
import provideJestResult from '../hoc/provideJestResult'; | ||
import colors from '../colors'; | ||
|
||
const List = glamorous.ul({ | ||
listStyle: 'none', | ||
fontSize: 14, | ||
padding: 0, | ||
margin: '10px 0', | ||
}); | ||
|
||
const Item = glamorous.li({ | ||
display: 'block', | ||
margin: '10px 0', | ||
padding: 0, | ||
}); | ||
|
||
const NoTests = glamorous.div({ | ||
padding: '10px 20px', | ||
flex: 1, | ||
...baseFonts, | ||
}); | ||
|
||
const FileTitle = glamorous.h2({ | ||
margin: 0, | ||
fontWeight: 500, | ||
fontSize: 18, | ||
}); | ||
|
||
const SuiteHead = glamorous.div({ | ||
display: 'flex', | ||
alignItems: 'baseline', | ||
justifyContent: 'space-between', | ||
position: 'relative', | ||
paddingTop: 10, | ||
}); | ||
|
||
const SuiteTotals = glamorous(({ successNumber, failedNumber, result, className }) => ( | ||
<div className={className}> | ||
{successNumber > 0 && <div style={{ color: colors.success }}>{successNumber} passed</div>} | ||
{failedNumber > 0 && <div style={{ color: colors.error }}>{failedNumber} failed</div>} | ||
<div>{result.assertionResults.length} total</div> | ||
<div> | ||
<strong>{result.endTime - result.startTime}ms</strong> | ||
</div> | ||
</div> | ||
))({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
color: colors.grey, | ||
fontSize: '10px', | ||
|
||
'& > *': { | ||
marginLeft: 10, | ||
}, | ||
}); | ||
|
||
const SuiteProgress = glamorous(({ successNumber, result, className }) => ( | ||
<div className={className} role="progressbar"> | ||
<span style={{ width: `${successNumber / result.assertionResults.length * 100}%` }}> | ||
{`${successNumber / result.assertionResults.length * 100}%`} | ||
</span> | ||
</div> | ||
))(() => ({ | ||
width: '100%', | ||
backgroundColor: colors.error, | ||
height: 4, | ||
top: 0, | ||
position: 'absolute', | ||
left: 0, | ||
borderRadius: 3, | ||
overflow: 'hidden', | ||
appearance: 'none', | ||
|
||
'& > span': { | ||
backgroundColor: colors.success, | ||
bottom: 0, | ||
position: 'absolute', | ||
left: 0, | ||
top: 0, | ||
boxShadow: '4px 0 0 white', | ||
}, | ||
})); | ||
|
||
const SuiteTitle = glamorous.div({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
}); | ||
|
||
const Content = glamorous(({ tests, className }) => ( | ||
<div className={className}> | ||
{tests.map(({ name, result }) => { | ||
if (!result) { | ||
return <NoTests>This story has tests configures, but no file not found</NoTests>; | ||
} | ||
|
||
const successNumber = result.assertionResults.filter(({ status }) => status === 'passed') | ||
.length; | ||
const failedNumber = result.assertionResults.length - successNumber; | ||
|
||
return ( | ||
<section key={name}> | ||
<SuiteHead> | ||
<SuiteTitle> | ||
<Indicator | ||
color={result.status === 'passed' ? colors.success : colors.error} | ||
size={16} | ||
styles={{ marginRight: 5 }} | ||
> | ||
{result.status} | ||
</Indicator> | ||
<FileTitle>{name}</FileTitle> | ||
</SuiteTitle> | ||
<SuiteTotals {...{ successNumber, failedNumber, result }} /> | ||
<SuiteProgress {...{ successNumber, failedNumber, result }} /> | ||
</SuiteHead> | ||
<List> | ||
{result.assertionResults.map(res => ( | ||
<Item key={res.fullName || res.title}> | ||
{res.failureMessages && res.failureMessages.length ? ( | ||
<FailedResult {...res} /> | ||
) : ( | ||
<Result {...res} /> | ||
)} | ||
</Item> | ||
))} | ||
</List> | ||
</section> | ||
); | ||
})} | ||
</div> | ||
))({ | ||
padding: '10px 20px', | ||
flex: '1 1 0%', | ||
...baseFonts, | ||
}); | ||
|
||
const Panel = ({ tests }) => | ||
tests ? <Content tests={tests} /> : <NoTests>This story has no tests configures</NoTests>; | ||
|
||
Panel.defaultProps = { | ||
tests: null, | ||
}; | ||
|
||
Panel.propTypes = { | ||
tests: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
result: PropTypes.object, | ||
}) | ||
), | ||
}; | ||
|
||
export default provideJestResult(Panel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@renaudtertrais @ndelangen why do we even have this recommendation? Checking this file into VCS makes a lot of sense to me, given that it's used in an
import
. What's the downside?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it will likely have tons of merge-conflicts over time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just like lockfiles and test snapshots. It's easy to regenerate them, so this shouldn't be a problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true
I will change the text and make it so users are aware of the trade-offs.