Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor addon-jest to use a parameter-based pattern #3678

Merged
merged 6 commits into from
Jul 4, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 54 additions & 42 deletions addons/jest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@ When running **Jest**, be sure to save the results in a json file:
```

You may want to add it the result file to `.gitignore`, since it's a generated file:

```
jest-test-results.json
```

But much like lockfiles and snapshots checking-in generated files can have certain advantages as well. It's up to you.
We recommend to **do** check in the test results file so starting storybook from an clean git clone doesn't require running all tests first,
but this can mean you'll experience merge conflicts on this file in the future. (*re-generating this file is super easy though, just like lockfiles and snapshots*)
We recommend to **do** check in the test results file so starting storybook from an clean git clone doesn't require running all tests first,
but this can mean you'll experience merge conflicts on this file in the future. (_re-generating this file is super easy though, just like lockfiles and snapshots_)

## Generating the test results

You need to make sure the generated test-restuls file exists before you start storybook.
During development you will likely start jest in watch-mode
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
Expand All @@ -50,9 +52,10 @@ 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,
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",
Expand Down Expand Up @@ -83,48 +86,55 @@ import results from '../.jest-test-results.json';
import { withTests } from '@storybook/addon-jest';

storiesOf('MyComponent', module)
.addDecorator(withTests({ results })('MyComponent', 'MyOtherComponent'))
.add('This story shows test results from MyComponent.test.js and MyOtherComponent.test.js', () => (
<div>Jest results in storybook</div>
));
.addDecorator(withTests({ results }))
.add(
'This story shows test results from MyComponent.test.js and MyOtherComponent.test.js',
() => <div>Jest results in storybook</div>,
{
jest: ['MyComponent.test.js', 'MyOtherComponent.test.js'],
}
);
```

Or in order to avoid importing `.jest-test-results.json` in each story, you can create a simple file `withTests.js`:
Or in order to avoid importing `.jest-test-results.json` in each story, simply add the decorator in your `.storybook/config.js` and results will display for stories that you have set the `jest` parameter on:

```js
import results from '../.jest-test-results.json';
import { addDecorator } from '@storybook/react'; // <- or your view layer
import { withTests } from '@storybook/addon-jest';

export default withTests({
results,
});
import results from '../.jest-test-results.json';

addDecorator(
withTests({
results,
})
);
```

Then in your story:

```js
// import your file
import withTests from '.withTests';

storiesOf('MyComponent', module)
.addDecorator(withTests('MyComponent', 'MyOtherComponent'))
.add('This story shows test results from MyComponent.test.js and MyOtherComponent.test.js', () => (
<div>Jest results in storybook</div>
));
// Use .addParameters if you want the same tests displayed for all stories of the component
.addParameters({ jest: ['MyComponent', 'MyOtherComponent'] })
.add(
'This story shows test results from MyComponent.test.js and MyOtherComponent.test.js',
() => <div>Jest results in storybook</div>
);
```

### withTests(options)

- **options.results**: OBJECT jest output results. *mandatory*
- **filesExt**: STRING test file extention. *optionnal*. This allow you to write "MyComponent" and not "MyComponent.test.js". It will be used as regex to find your file results. Default value is `((\\.specs?)|(\\.tests?))?(\\.js)?$`. That mean it will match: MyComponent.js, MyComponent.test.js, MyComponent.tests.js, MyComponent.spec.js, MyComponent.specs.js...
* **options.results**: OBJECT jest output results. _mandatory_
* **filesExt**: STRING test file extention. _optionnal_. This allow you to write "MyComponent" and not "MyComponent.test.js". It will be used as regex to find your file results. Default value is `((\\.specs?)|(\\.tests?))?(\\.js)?$`. That mean it will match: MyComponent.js, MyComponent.test.js, MyComponent.tests.js, MyComponent.spec.js, MyComponent.specs.js...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionnal => optional 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was an existing line ;)


## Usage with Angular

Assuming that you have created a test files `my.component.spec.ts` and `my-other.comonent.spec.ts`

Configure Jest with [jest-preset-angular](https://www.npmjs.com/package/jest-preset-angular)

In project`s `typings.d.ts` add
In project`s`typings.d.ts` add

```ts
declare module '*.json' {
Expand All @@ -133,41 +143,43 @@ declare module '*.json' {
}
```

Create a simple file `withTests.ts`:
In your `.storybook/config.ts`:

```ts
import * as results from '../.jest-test-results.json';
import { addDecorator } from '@storybook/angular';
import { withTests } from '@storybook/addon-jest';

export const wTests = withTests({
results,
filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$'
});
import * as results from '../.jest-test-results.json';

addDecorator(
withTests({
results,
filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$',
})
);
```

Then in your story:

```js
// import your file
import wTests from '.withTests';

storiesOf('MyComponent', module)
.addDecorator(wTests('my.component', 'my-other.component'))
.add('This story shows test results from my.component.spec.ts and my-other.component.spec.ts', () => (
<div>Jest results in storybook</div>
));
.addParameters({ jest: ['my.component', 'my-other.component'] })
.add(
'This story shows test results from my.component.spec.ts and my-other.component.spec.ts',
() => <div>Jest results in storybook</div>
);
```

##### Example [here](https://github.com/storybooks/storybook/tree/master/examples/angular-cli)

## TODO

- [ ] Add coverage
- [ ] Display nested test better (describe)
- [ ] Display the date of the test
- [ ] Add unit tests
- [ ] Add linting
- [ ] Split <TestPanel />
* [ ] Add coverage
* [ ] Display nested test better (describe)
* [ ] Display the date of the test
* [ ] Add unit tests
* [ ] Add linting
* [ ] Split <TestPanel />

## Contributing

Expand Down
3 changes: 2 additions & 1 deletion addons/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"emotion": "^9.1.3",
"global": "^4.3.2",
"prop-types": "^15.6.1",
"react-emotion": "^9.1.3"
"react-emotion": "^9.1.3",
"util-deprecate": "^1.0.2"
},
"peerDependencies": {
"react": "*"
Expand Down
27 changes: 23 additions & 4 deletions addons/jest/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import addons from '@storybook/addons';
import deprecate from 'util-deprecate';

const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) =>
testFiles.map(name => {
[].concat(testFiles).map(name => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ? map() is immutable no ?

Copy link
Member Author

@tmeasday tmeasday May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[].concat() is a way to "ensure array" -- [].concat('a') ~== [].concat(['a'])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it lets the user set { jest: 'filename' } or { jest: ['filename', 'other-filename'] } which is natural.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.from(testFiles) is a more explicit way to do the same

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I didn't know. Good trick.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hypnosphi -- good one, I'll make that change.

if (jestTestResults && jestTestResults.testResults) {
return {
name,
Expand All @@ -27,9 +28,27 @@ export const withTests = userOptions => {
};
const options = Object.assign({}, defaultOptions, userOptions);

return (...testFiles) => (storyFn, { kind, story }) => {
emitAddTests({ kind, story, testFiles, options });
return (...args) => {
if (typeof args[0] === 'string') {
return deprecate((story, { kind }) => {
emitAddTests({ kind, story, testFiles: args, options });

return storyFn();
return story();
}, 'Passing component filenames to the `@storybook/addon-jest` via `withTests` is deprecated. Instead, use the `jest` story parameter');
}

const [
story,
{
kind,
parameters: { jest: testFiles },
},
] = args;

if (testFiles && !testFiles.skip) {
Copy link
Contributor

@Keraito Keraito Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this testFiles.skip come from? Is this the same as the disable flag that we have in the other parameterized addons? It's not really documented anywhere.

Copy link
Contributor

@Keraito Keraito Jun 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poking @tmeasday just in case you missed this comment, since this is part of the 4.0 release. If you didn't, just ignore this message!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Keraito -- I have not forgotten, just getting around to this, sorry!

emitAddTests({ kind, story, testFiles, options });
}

return story();
};
};
7 changes: 0 additions & 7 deletions examples/angular-cli/.storybook/withTests.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/angular-cli/addon-jest.testresults.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":2,"numPassedTests":6,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTotalTestSuites":2,"numTotalTests":6,"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeys":[],"unmatched":0,"updated":0},"startTime":1527433445200,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should create the app","location":null,"status":"passed","title":"should create the app"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should have as title 'app'","location":null,"status":"passed","title":"should have as title 'app'"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should render title in a h1 tag","location":null,"status":"passed","title":"should render title in a h1 tag"}],"endTime":1527433449835,"message":"","name":"/Users/jetbrains/IdeaProjects/storybook/examples/angular-cli/src/app/app.component.spec.ts","startTime":1527433446968,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should create the app","location":null,"status":"passed","title":"should create the app"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should have as title 'app'","location":null,"status":"passed","title":"should have as title 'app'"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should render title in a h1 tag","location":null,"status":"passed","title":"should render title in a h1 tag"}],"endTime":1527433449838,"message":"","name":"/Users/jetbrains/IdeaProjects/storybook/examples/angular-cli/dist/app/app.component.spec.ts","startTime":1527433446974,"status":"passed","summary":""}],"wasInterrupted":false}
{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":2,"numPassedTests":6,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTotalTestSuites":2,"numTotalTests":6,"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeys":[],"unmatched":0,"updated":0},"startTime":1527573039637,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should create the app","location":null,"status":"passed","title":"should create the app"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should have as title 'app'","location":null,"status":"passed","title":"should have as title 'app'"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should render title in a h1 tag","location":null,"status":"passed","title":"should render title in a h1 tag"}],"endTime":1527573042008,"message":"","name":"/Users/tom/OSS/storybook/examples/angular-cli/dist/app/app.component.spec.ts","startTime":1527573040569,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should create the app","location":null,"status":"passed","title":"should create the app"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should have as title 'app'","location":null,"status":"passed","title":"should have as title 'app'"},{"ancestorTitles":["AppComponent"],"failureMessages":[],"fullName":"AppComponent should render title in a h1 tag","location":null,"status":"passed","title":"should render title in a h1 tag"}],"endTime":1527573042010,"message":"","name":"/Users/tom/OSS/storybook/examples/angular-cli/src/app/app.component.spec.ts","startTime":1527573040569,"status":"passed","summary":""}],"wasInterrupted":false}
25 changes: 19 additions & 6 deletions examples/angular-cli/src/stories/addon-jest.stories.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { storiesOf } from '@storybook/angular';
import { withTests } from '@storybook/addon-jest';

import { AppComponent } from '../app/app.component';
import { wTests } from '../../.storybook/withTests';
import * as results from '../../addon-jest.testresults.json';

storiesOf('Addon|Jest', module)
.addDecorator(wTests('app.component'))
.add('app.component with jest tests', () => ({
component: AppComponent,
props: {},
}));
.addDecorator(
withTests({
results,
filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$',
})
)
.add(
'app.component with jest tests',
() => ({
component: AppComponent,
props: {},
}),
{
jest: 'app.component',
}
);
8 changes: 2 additions & 6 deletions examples/html-kitchen-sink/stories/addon-jest.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { storiesOf } from '@storybook/html';
import { withTests } from '@storybook/addon-jest';
import results from './addon-jest.testresults.json';

const withTestsFiles = withTests({
results,
});

storiesOf('Addons|jest', module)
.addDecorator(withTestsFiles('addon-jest'))
.add('withTests', () => 'This story shows test results');
.addDecorator(withTests({ results }))
.add('withTests', () => 'This story shows test results', { jest: 'addon-jest' });
20 changes: 10 additions & 10 deletions examples/official-storybook/stories/addon-jest.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { storiesOf } from '@storybook/react';
import { withTests } from '@storybook/addon-jest';
import results from './addon-jest.testresults.json';

const withTestsFiles = withTests({
results,
});

storiesOf('Addons|jest', module)
.addDecorator(withTestsFiles('addon-jest'))
.add('withTests', () => (
<div>
<p>Hello</p>
</div>
));
.addDecorator(withTests({ results }))
.add(
'withTests',
() => (
<div>
<p>Hello</p>
</div>
),
{ jest: 'addon-jest' }
);