pulled release/3.3

This commit is contained in:
Syneva Runyan 2017-11-22 21:06:25 -05:00
commit ffcd4eb6a5
783 changed files with 32856 additions and 17124 deletions

View File

@ -68,6 +68,7 @@ jobs:
command: |
cd examples/vue-kitchen-sink
yarn build-storybook
- run:
name: "Run react kitchen-sink"
command: |
@ -198,6 +199,54 @@ jobs:
command: |
yarn test --coverage --runInBand --core
yarn coverage
cli:
working_directory: /tmp/storybook
docker:
- image: andthensome/docker-node-rsync
environment:
BASH_ENV: ~/.bashrc
steps:
- checkout
- restore_cache:
keys:
- dependencies-{{ checksum "yarn.lock" }}
- dependencies-
- run:
name: "Install dependencies"
command: |
yarn install
- run:
name: "Bootstrapping"
command: |
yarn bootstrap --core
- run:
name: "Testing CLI"
command: |
yarn test --cli
cli-latest-cra:
working_directory: /tmp/storybook
docker:
- image: andthensome/docker-node-rsync
environment:
BASH_ENV: ~/.bashrc
steps:
- checkout
- restore_cache:
keys:
- dependencies-{{ checksum "yarn.lock" }}
- dependencies-
- run:
name: "Install dependencies"
command: |
yarn install
- run:
name: "Bootstrapping"
command: |
yarn bootstrap --core
- run:
name: "Testing CLI with latest CR(N)A"
command: |
yarn test-latest-cra
deploy:
<<: *defaults
steps:
@ -217,9 +266,7 @@ workflows:
requires:
- build
- docs
- lint:
requires:
- build
- unit-test:
requires:
- build
- lint
- unit-test
- cli
- cli-latest-cra

View File

@ -5,7 +5,7 @@ node_modules
addons/**/example/**
app/**/demo/**
docs/public
lib/cli/test
*.bundle.js
*.js.map

1
.gitignore vendored
View File

@ -2,6 +2,7 @@ node_modules
*.log
.idea
.vscode
*.sw*
npm-shrinkwrap.json
dist
.tern-port

50
.mailmap Normal file
View File

@ -0,0 +1,50 @@
# --- instructions --- #
# Add your account in this format:
Your name here <yourname@example.com> # github:my-github-account, npm:my-npm-account, twitter:my-twitter-handle
# supported:
# github, npm, twitter, website
# --- list ----------- #
Aaron Mc Adam <aaron@aaronmcadam.com>
Aruna Herath <aruna@kadira.io> <arunabherath@gmail.com>
Arunoda Susiripala <arunoda.susiripala@gmail.com> Arunoda Susiripala <arunoda.susiripala@gmail.com>
Benedikt D Valdez <benediktvaldez@users.noreply.github.com> Benedikt D Valdez <benediktvaldez@users.noreply.github.com>
Daniel Duan <dduan@squarespace.com> <dduan@yahoo.com>
Daniel James <daniel@thzinc.com> <djames@syncromatics.com>
Danny Andrews <danny-andrews@users.noreply.github.com> danny@ownlocal.com>
Dustin Kane <dkane@athenahealth.com> <dustinpkane@gmail.com>
Eli Sherer <eli.sherer@gmail.com> elish <elish@payoneer.com>
Evgeny Kochetkov <evgeny.kochetkov@me.com> Evgeny Kochetkov <evgenykochetkov@users.noreply.github.com>
Fabien Bernard <fabien0102@hotmail.com> Fabien BERNARD <fabien0102@hotmail.com>
Fernando Daciuk <f.daciuk@gmail.com> <fdaciuk@users.noreply.github.com>
Greenkeeper <support@greenkeeper.io> greenkeeper[bot] <greenkeeper[bot]@users.noreply.github.com>
Greenkeeper <support@greenkeeper.io> greenkeeperio-bot <support@greenkeeper.io>
Jason Schloer <jschloer@Jasons-Mac-Pro.local> jschloer <jschloer@terragotech.com>
Jean-Michel Francois <jmfrancois@talend.com> Jean-Michel FRANCOIS <jmfrancois@talend.com>
Jeff Carbonella <jeff.carbonella@gmail.com> <jeff@contactually.com>
Jeff Knaggs <jeef3@users.noreply.github.com> <mail@jeef3.com>
Jordan Gensler <jordan.gensler@airbnb.com> <jordangens@gmail.com>
Kanitkorn Sujautra <k.sujautra@gmail.com> Kanitkorn S <lukyth@users.noreply.github.com>
Kent C. Dodds <kent@doddsfamily.us> <kent+github@doddsfamily.us>
larry <bshy522@gmail.com> <larry@yunify.com>
Madushan Nishantha <j.l.madushan@gmail.com> <madushan1000@users.noreply.github.com>
Marie-Laure Thuret <mthuret@users.noreply.github.com> mthuret <marielaure.thuret@algolia.com>
Max Hodges <max@whiterabbitjapan.com> MaxHodges <max@whiterabbitjapan.com>
Michael Shilman <shilman@lab80.co> <shilman@users.noreply.github.com>
Michael Shilman <shilman@lab80.co> <michael@lab80.co>
Muhammed Thanish <mnmtanish@gmail.com> <mnmtanish@users.noreply.github.com>
Ned Schwartz <ned@theinterned.net> Ned Schwartz <ned@theinterned.net>
Joe Nelson <Joe.Nelson@regence.com> Nelson, Joe <Joe.Nelson@regence.com>
Nikolay Kozhuharenko <Nikolay.Kozhuharenko@gmail.com> Nikolay <Nikolay.Kozhuharenko@gmail.com>
Norbert de Langen <ndelangen@me.com> # github:ndelangen, npm:ndelangen, twitter:norbertdelangen
Oleg Proskurin <regx@usul.su> UsulPro <regx@usul.su>
Orta <orta.therox@gmail.com> orta <orta.therox@gmail.com>
Ritesh Kumar <ritz078@users.noreply.github.com> Ritesh Kumar <rkritesh078@gmail.com>
Sylvain Bannier <sylvain.bannier@smile.fr> Sylvain BANNIER <sylvain.bannier@smile.fr>
Tom Coleman <tom@percolatestudio.com> Tom Coleman <tom@thesnail.org>
Trevor Eyre <trevoreyre@gmail.com> # github:TrevorEyre, twitter:trevor_eyre
William Castandet <wcastand@gmail.com> wcastand <wcastand@gmail.com>
Xavier Cazalot <xavier.cazalot@gmail.com> xavcz <xavier.cazalot@gmail.com>

View File

@ -1,101 +1,104 @@
# 3.2.16
# 3.3.0-alpha.2
2017-November-15
2017-October-03
#### Features
- Add addon-a11y to monorepo [#2292](https://github.com/storybooks/storybook/pull/2292)
- Ability for custom storyshots testFunctions to utilise "snapshot per story file" [#1841](https://github.com/storybooks/storybook/pull/1841)
- Viewport Addon [#1753](https://github.com/storybooks/storybook/pull/1753)
- More detailed props table [#1485](https://github.com/storybooks/storybook/pull/1485)
- RN: Add accessibility labels to OnDeviceUI [#1780](https://github.com/storybooks/storybook/pull/1780)
- Have Stories on each level of hierarchy [#1763](https://github.com/storybooks/storybook/pull/1763)
- Viewport Addon [#1740](https://github.com/storybooks/storybook/pull/1740)
- Generate snapshot per story file [#1584](https://github.com/storybooks/storybook/pull/1584)
- addon-links: add `LinkTo` component, and `hrefTo` function [#1829](https://github.com/storybooks/storybook/pull/1829)
#### Bug Fixes
- Addon actions: replace eval with function name assignment [#2321](https://github.com/storybooks/storybook/pull/2321)
- FIX propwarning on basebutton && ADD style prop on basebutton [#2305](https://github.com/storybooks/storybook/pull/2305)
- React-native: fix drawer width [#2300](https://github.com/storybooks/storybook/pull/2300)
- CLI: Use actions in sample stories for vue + fix them in SFC_VUE template [#1867](https://github.com/storybooks/storybook/pull/1867)
- Improve rendering of 'types' in addon-actions [#1887](https://github.com/storybooks/storybook/pull/1887)
- Circular json can possibly hang [#1881](https://github.com/storybooks/storybook/pull/1881)
- Use HtmlWebpackPlugin to import all assets (importing chunks in order) [#1775](https://github.com/storybooks/storybook/pull/1775)
- Fix preview scrolling [#1782](https://github.com/storybooks/storybook/pull/1782)
- Search box: make found options selectable with click [#1697](https://github.com/storybooks/storybook/pull/1697)
- Fix Docgen in static builds for Info [#1725](https://github.com/storybooks/storybook/pull/1725)
- Return empty array when Array knob is empty [#1811](https://github.com/storybooks/storybook/pull/1811)
#### Documentation
- Make dependencies more deterministic [#1703](https://github.com/storybooks/storybook/pull/1703)
- Synced changes from new-docs to CONTRIBUTING.md [#1911](https://github.com/storybooks/storybook/pull/1911)
- Fix incorrect yarn command in docs [#1758](https://github.com/storybooks/storybook/pull/1758)
#### Maintenance
- Add Previews of deployed examples via Netlify [#2304](https://github.com/storybooks/storybook/pull/2304)
- Drop "Install latest yarn version" step on CI [#1910](https://github.com/storybooks/storybook/pull/1910)
- CLI: A more human-friendly message for undetected project types [#1825](https://github.com/storybooks/storybook/pull/1825)
- CLI: handle promise rejections [#1826](https://github.com/storybooks/storybook/pull/1826)
- Add tests for CLI [#1767](https://github.com/storybooks/storybook/pull/1767)
- Yarn workspaces [#1810](https://github.com/storybooks/storybook/pull/1810)
- Knobs: allow arrays in object knob proptypes [#1701](https://github.com/storybooks/storybook/pull/1701)
- Deprecate confusing option names [#1692](https://github.com/storybooks/storybook/pull/1692)
- A CLI for running specific tests suites, like bootstrap CLI [#1752](https://github.com/storybooks/storybook/pull/1752)
- Remove check for sender on channel. [#1407](https://github.com/storybooks/storybook/pull/1407)
- Exit with code 1 if `start-storybook --smoke-test` fails [#1851](https://github.com/storybooks/storybook/pull/1851)
- Refactor CLI [#1840](https://github.com/storybooks/storybook/pull/1840)
- Refactor knobs - no longer include all runtimes [#1832](https://github.com/storybooks/storybook/pull/1832)
- Added addon-knobs to crna and vanilla react native. [#1636](https://github.com/storybooks/storybook/pull/1636)
#### Dependency Upgrades
<details>
<summary>
30 upgrades
</summary>
- Add config for dependencies.io [#1770](https://github.com/storybooks/storybook/pull/1770)
- Update 5 dependencies from npm [#2312](https://github.com/storybooks/storybook/pull/2312)
- Upgraded gatsby-link in `docs` from `1.6.27` to `1.6.28` [#2311](https://github.com/storybooks/storybook/pull/2311)
- Upgraded gatsby-plugin-sharp in `docs` from `1.6.20` to `1.6.21` [#2311](https://github.com/storybooks/storybook/pull/2311)
- Upgraded gatsby-remark-images in `docs` from `1.5.31` to `1.5.32` [#2311](https://github.com/storybooks/storybook/pull/2311)
- Upgraded gatsby in `docs` from `1.9.108` to `1.9.112` [#2308](https://github.com/storybooks/storybook/pull/2308)
- Upgraded gatsby-link in `docs` from `1.6.26` to `1.6.27` [#2308](https://github.com/storybooks/storybook/pull/2308)
- Upgraded gatsby-remark-copy-linked-files in `docs` from `1.5.20` to `1.5.21` [#2308](https://github.com/storybooks/storybook/pull/2308)
- Upgraded gatsby-transformer-remark in `docs` from `1.7.20` to `1.7.21` [#2308](https://github.com/storybooks/storybook/pull/2308)
- Upgraded react-textarea-autosize in `addons/events` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
- Upgraded react-datetime in `addons/knobs` from `2.10.3` to `2.11.0` [#2309](https://github.com/storybooks/storybook/pull/2309)
- Upgraded react-textarea-autosize in `addons/knobs` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
- Upgraded react-textarea-autosize in `addons/comments` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
- Upgraded moment in `addons/knobs` from `2.19.1` to `2.19.2` [#2293](https://github.com/storybooks/storybook/pull/2293)
- Upgraded moment in `addons/comments` from `2.19.1` to `2.19.2` [#2293](https://github.com/storybooks/storybook/pull/2293)
- Upgraded gatsby in `docs` from `1.9.100` to `1.9.108` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-link in `docs` from `1.6.24` to `1.6.26` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-plugin-sharp in `docs` from `1.6.19` to `1.6.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-remark-autolink-headers in `docs` from `1.4.7` to `1.4.8` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-remark-copy-linked-files in `docs` from `1.5.16` to `1.5.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-remark-images in `docs` from `1.5.30` to `1.5.31` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-source-filesystem in `docs` from `1.5.7` to `1.5.8` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Upgraded gatsby-transformer-remark in `docs` from `1.7.19` to `1.7.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
- Update lint-staged to 5.0.0 [#2291](https://github.com/storybooks/storybook/pull/2291)
- Upgraded eslint in `/` from `4.10.0` to `4.11.0` [#2290](https://github.com/storybooks/storybook/pull/2290)
- Upgraded puppeteer in `/` from `0.12.0` to `0.13.0` [#2290](https://github.com/storybooks/storybook/pull/2290)
- Update 6 dependencies from npm [#2286](https://github.com/storybooks/storybook/pull/2286)
- Update React to 16.1.0 [#2285](https://github.com/storybooks/storybook/pull/2285)
- Update 4 dependencies from npm [#2284](https://github.com/storybooks/storybook/pull/2284)
- use @storybook published deprecated dependencies [#2314](https://github.com/storybooks/storybook/pull/2314)
- Update inquirer to 4.0.0 [#2298](https://github.com/storybooks/storybook/pull/2298)
# 3.3.0-alpha.0
</details>
# 3.2.15
2017-November-10
2017-September-06
#### Features
- Optimizing for iphone x [#2260](https://github.com/storybooks/storybook/pull/2260)
- Fix accessibility warnings [#2270](https://github.com/storybooks/storybook/pull/2270)
- Viewport addon: simulate device sizes in preview window [#1753](https://github.com/storybooks/storybook/pull/1753)
- CLI: Add codemod for deprecated addon-links and addon-actions from app [#1368](https://github.com/storybooks/storybook/pull/1368)
- Info addon: More detailed props table [#1485](https://github.com/storybooks/storybook/pull/1485)
- React native: Add accessibility labels to OnDeviceUI [#1780](https://github.com/storybooks/storybook/pull/1780)
- Stories panel: Stories on each hierarchy level [#1763](https://github.com/storybooks/storybook/pull/1763)
- Storyshots: Generate snapshot per story file [#1584](https://github.com/storybooks/storybook/pull/1584)
- CLI: Add support for Vue projects using Nuxt [#1794](https://github.com/storybooks/storybook/pull/1794)
#### Bug Fixes
- Fix propTypes in addon-background [#2279](https://github.com/storybooks/storybook/pull/2279)
- Addon-info: allow duplicate displayNames [#2269](https://github.com/storybooks/storybook/pull/2269)
- Fix browser navigation [#2261](https://github.com/storybooks/storybook/pull/2261)
- Import chunks/assets in correct order using HtmlWebpackPlugin [#1775](https://github.com/storybooks/storybook/pull/1775)
- Fix preview scrolling [#1782](https://github.com/storybooks/storybook/pull/1782)
- Height aligned 2 buttons in manager's header [#1769](https://github.com/storybooks/storybook/pull/1769)
- Search box: make found options selectable with click [#1697](https://github.com/storybooks/storybook/pull/1697)
- Info addon: Fix Docgen in static builds [#1725](https://github.com/storybooks/storybook/pull/1725)
- Knobs: allow arrays in object knob proptypes [#1701](https://github.com/storybooks/storybook/pull/1701)
#### Documentation
- Improve linkTo documentation [#1793](https://github.com/storybooks/storybook/pull/1793)
- Add carbon to examples page [#1764](https://github.com/storybooks/storybook/pull/1764)
- Minor grammar fixes and clarification to Vue documentation [#1756](https://github.com/storybooks/storybook/pull/1756)
- Fix incorrect yarn command in docs [#1758](https://github.com/storybooks/storybook/pull/1758)
- Add storybook-chrome-screenshot to addon gallery [#1761](https://github.com/storybooks/storybook/pull/1761)
- Fixing typo on VueJS withNotes Example [#1787](https://github.com/storybooks/storybook/pull/1787)
#### Maintenance
- Fixes to build scripts for Windows. [#2051](https://github.com/storybooks/storybook/pull/2051)
- Update dependencies.yml to include batch updates for docs dependencies [#2252](https://github.com/storybooks/storybook/pull/2252)
- Deprecate confusing option names [#1692](https://github.com/storybooks/storybook/pull/1692)
- A CLI for running specific tests suites, like bootstrap CLI [#1752](https://github.com/storybooks/storybook/pull/1752)
- Remove check for sender on channel. [#1407](https://github.com/storybooks/storybook/pull/1407)
- Use yarn instead of NPM [#1703](https://github.com/storybooks/storybook/pull/1703)
- Add config for dependencies.io [#1770](https://github.com/storybooks/storybook/pull/1770)
- Added addon-knobs to crna and vanilla react native. [#1636](https://github.com/storybooks/storybook/pull/1636)
- Fixed Jest warnings [#1744](https://github.com/storybooks/storybook/pull/1744)
- Smoke test master [#1801](https://github.com/storybooks/storybook/pull/1801)
#### Dependency Upgrades
<details>
<summary>
11 PRs
</summary>
- Update 4 dependencies from npm [#2267](https://github.com/storybooks/storybook/pull/2267)
- Update 8 dependencies from npm [#2262](https://github.com/storybooks/storybook/pull/2262)
- Update 3 dependencies from npm [#2257](https://github.com/storybooks/storybook/pull/2257)
- Update babel-eslint in / from 8.0.1 to 8.0.2 [#2253](https://github.com/storybooks/storybook/pull/2253)
- 3 packages updated by dependencies.io [#2251](https://github.com/storybooks/storybook/pull/2251)
- Update devDependencies [#2232](https://github.com/storybooks/storybook/pull/2232)
- Update react-textarea-autosize to 5.1.0 [#2233](https://github.com/storybooks/storybook/pull/2233)
- Update insert-css to 2.0.0 [#2234](https://github.com/storybooks/storybook/pull/2234)
- Update file-loader to 1.1.5 [#2236](https://github.com/storybooks/storybook/pull/2236)
- Update read-pkg-up to 3.0.0 [#2237](https://github.com/storybooks/storybook/pull/2237)
- Update react-modal to 3.1.0 [#2238](https://github.com/storybooks/storybook/pull/2238)
</details>
- Upgrade root dependencies and sync with packages [#1802](https://github.com/storybooks/storybook/pull/1802)
- Update jest to the latest version 🚀 [#1799](https://github.com/storybooks/storybook/pull/1799)
- Update eslint-plugin-jest to the latest version 🚀 [#1795](https://github.com/storybooks/storybook/pull/1795)
- Update lerna to the latest version 🚀 [#1768](https://github.com/storybooks/storybook/pull/1768)
# 3.2.14

View File

@ -22,7 +22,7 @@ No software is bug free. So, if you got an issue, follow these steps:
To test your project against the current latest version of storybook, you can clone the repository and link it with `yarn`. Try following these steps:
1. Download the latest version of this project, and build it:
#### 1. Download the latest version of this project, and build it:
```sh
git clone https://github.com/storybooks/storybook.git
@ -31,46 +31,87 @@ yarn install
yarn bootstrap --core
```
2. Link `storybook` and any other required dependencies:
The bootstrap command will ask which sections of the codebase you want to bootstrap. Unless you're going to work with ReactNative or the Documentation, you can keep the default.
You can also pick directly from CLI:
yarn bootstrap --core
#### 2a. Run unit tests
You can use one of the example projects in `examples/` to develop on.
This command will list all the suites and options for running tests.
```sh
cd app/react
yarn link
cd <your-project>
yarn link @storybook/react
# repeat with whichever other parts of the monorepo you are using.
yarn test
```
### Reproductions
_Note that in order to run the tests fro ReactNative, you must have bootstrapped with ReactNative enabled_
The best way to help figure out an issue you are having is to produce a minimal reproduction against the `master` branch.
A good way to do that is using the example `cra-kitchen-sink` app embedded in this repository:
You can also pick suites from CLI:
```sh
# Download and build this repository:
git clone https://github.com/storybooks/storybook.git
cd storybook
yarn install
yarn bootstrap --core
cd examples/cra-kitchen-sink
# make changes to try and reproduce the problem, such as adding components + stories
yarn storybook
# see if you can see the problem, if so, commit it:
git checkout "branch-describing-issue"
git add -A
git commit -m "reproduction for issue #123"
# fork the storybook repo to your account, then add the resulting remote
git remote add <your-username> https://github.com/<your-username>/storybook.git
git push -u <your-username> master
yarn test --core
```
In order to run ALL unit tests, you must have bootstrapped the react-native
#### 2b. Run e2e tests for CLI
If you made any changes to `lib/cli` package, the easiest way to verify that it doesn't break anything is to run e2e tests:
yarn test --cli
This will run a bash script located at `lib/cli/test/run_tests.sh`. It will copy the contents of `fixtures` into a temporary `run` directory, run `getstorybook` in each of the subdirectories, and check that storybook starts successfully using `yarn storybook --smoke-test`.
After that, the `run` directory content will be compared with `snapshots`. You can update the snapshots by passing an `--update` flag:
yarn test --cli --update
In that case, please check the git diff before commiting to make sure it only contains the intended changes.
#### 2c. Link `storybook` and any other required dependencies:
If you want to test your own existing project using the github version of storybook, you need to `link` the packages you use in your project.
````sh
cd app/react
yarn link
cd <your-project>
yarn link @storybook/react
# repeat with whichever other parts of the monorepo you are using.
```
### Reproductions
The best way to help figure out an issue you are having is to produce a minimal reproduction against the `master` branch.
A good way to do that is using the example `cra-kitchen-sink` app embedded in this repository:
```sh
# Download and build this repository:
git clone https://github.com/storybooks/storybook.git
cd storybook
yarn install
yarn bootstrap --core
# make changes to try and reproduce the problem, such as adding components + stories
cd examples/cra-kitchen-sink
yarn storybook
# see if you can see the problem, if so, commit it:
git checkout "branch-describing-issue"
git add -A
git commit -m "reproduction for issue #123"
# fork the storybook repo to your account, then add the resulting remote
git remote add <your-username> https://github.com/<your-username>/storybook.git
git push -u <your-username> master
````
If you follow that process, you can then link to the github repository in the issue. See <https://github.com/storybooks/storybook/issues/708#issuecomment-290589886> for an example.
**NOTE**: If your issue involves a webpack config, create-react-app will prevent you from modifying the _app's_ webpack config, however you can still modify storybook's to mirror your app's version of storybook. Alternatively, use `yarn eject` in the CRA app to get a modifiable webpack config.
@ -186,51 +227,95 @@ If an issue is a `bug`, and it doesn't have a clear reproduction that you have p
> If you want to work on a UI feature, refer to the [Storybook UI](https://github.com/storybooks/storybook/tree/master/lib/ui) page.
This project written in ES2016+ syntax so, we need to transpile it before use.
So run the following command:
### Prerequisites
```sh
yarn dev
```
Please have the **_latest_** stable versions of the following on your machine
This will watch files and transpile in watch mode.
- node
- yarn
### Linking
### Initial Setup
First of all link this repo with:
If you run into trouble here, make sure your node, npm, and **_yarn_** are on the latest versions (yarn at least v1.0.0).
```sh
yarn link
```
1. `cd ~` (optional)
2. `git clone https://github.com/storybooks/storybook.git` _bonus_: use your own fork for this step
3. `cd storybook`
4. `yarn`
5. `yarn bootstrap --core`
6. `yarn test --core`
7. `yarn dev` _You must have this running for your changes to show up_
In order to test features you add, you may need to link the local copy of this repo.
For that we need a sample project. Let's create it.
#### Bootstrapping everything
```sh
yarn global add create-react-app getstorybook
create-react-app my-demo-app
cd my-demo-app
getstorybook
```
_This method is slow_
> It's pretty important to create a very simple sample project like above.
> Otherwise some of the functionality won't work because of linking.
1. `yarn bootstrap --all`
2. Have a beer 🍺
3. `yarn test` (to verify everything worked)
Then link storybook inside the sample project with:
### Working with the kitchen sink apps
```sh
yarn link @storybook/react
```
Within the `examples` folder of the Storybook repo, you will find kitchen sink examples of storybook implementations for the various platforms that storybook supports.
### Getting Changes
Not only do these show many of the options and addons available, they are also automatically linked to all the development packages. We highly encourage you to use these to develop/test contributions on.
After you've done any change, you need to run the `yarn storybook` command every time to see those changes.
#### React and Vue
1. `yarn storybook`
2. Verify that your local version works
### Working with your own app
#### Linking Storybook
Storybook is broken up into sub-projects that you can install as you need them. For this example we will be working with `@storybook/react`.
**Note:** You need to `yarn link` from inside the sub project you are working on **_NOT_** the storybook root directory
1. `cd app/react`
2. `yarn link`
#### Connecting Your App To Storybook
**_Note:_** If you aren't seeing addons after linking storybook, you probably have a versioning issue which can be fixed by simply linking each addon you want to use.
This applies for the kitchen sink apps as well as your own projects.
_Make sure `yarn dev` is running_
##### 1. Setup storybook in your project
First we are going to install storyboook, then we are going to link `@storybook/react` into our project. This will replace `node_modules/@storybook/react` with a symlink to our local version of storybook.
1. `getstorybook`
2. `yarn storybook`
3. Verify that your local version works
##### 2. Link
**_Note_**: This process is the same for `@storybook/vue`, `@storybook/addon-foo`, etc
1. Go to your storybook _root_ directory
2. `yarn dev`
3. Wait until the output stops (changes you make will be transpiled into dist and logged here)
4. Go to your storybook-sandbox-app directory
5. `yarn link @storybook/react`
6. `yarn storybook`
#### Verify your local version is working
You should now have a working storybook dev environment up and running. To verify this you can make changes to the following file:
`open app/react/src/client/manager/preview.js`
Save and go to `http://localhost:9009` (or wherever storybook is running)
If you don't see the changes rerun `yarn storybook` again in your sandbox app
## Release Guide
This section is for Storybook maintainers who will be creating releases. It assumes:
- yarn >= 1.0.0 (otherwise you should pass a -- before command arguments)
- yarn >= 1.0.0
- you've yarn linked `pr-log` from <https://github.com/storybooks/pr-log/pull/2>
The current manual release sequence is as follows:

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -21,9 +21,8 @@
"storybook": "start-storybook -p 9001"
},
"dependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/addons": "^3.3.0-alpha.2",
"deep-equal": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"prop-types": "^15.6.0",
"react-inspector": "^2.2.1",
"uuid": "^3.1.0"

View File

@ -5,7 +5,7 @@ import style from './style';
class ActionLogger extends Component {
getActionData() {
return this.props.actions.map((action, i) => this.renderAction(action, i));
return this.props.actions.map(action => this.renderAction(action));
}
renderAction(action) {
@ -15,7 +15,8 @@ class ActionLogger extends Component {
<div style={style.countwrap}>{action.count > 1 && counter}</div>
<div style={style.inspector}>
<Inspector
showNonenumerable
sortObjectKeys
showNonenumerable={false}
name={action.data.name}
data={action.data.args || action.data}
/>

View File

@ -3,6 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import deepEqual from 'deep-equal';
import { CYCLIC_KEY, isObject, retrocycle } from '../../util';
import ActionLoggerComponent from '../../components/ActionLogger/';
import { EVENT_ID } from '../../';
@ -23,10 +24,12 @@ export default class ActionLogger extends React.Component {
}
addAction(action) {
action.data.args = action.data.args.map(arg => JSON.parse(arg)); // eslint-disable-line
action.data.args = action.data.args.map(arg => retrocycle(arg)); // eslint-disable-line
const isCyclic = !!action.data.args.find(arg => isObject(arg) && arg[CYCLIC_KEY]);
const actions = [...this.state.actions];
const previous = actions.length && actions[0];
if (previous && deepEqual(previous.data, action.data, { strict: true })) {
if (previous && !isCyclic && deepEqual(previous.data, action.data, { strict: true })) {
previous.count++; // eslint-disable-line
} else {
action.count = 1; // eslint-disable-line

View File

@ -1,21 +1,14 @@
/* eslint-disable no-underscore-dangle */
import addons from '@storybook/addons';
import stringify from 'json-stringify-safe';
import uuid from 'uuid/v1';
import { EVENT_ID } from './';
function _format(arg) {
if (arg && typeof arg.preventDefault !== 'undefined') {
return stringify('[SyntheticEvent]');
}
return stringify(arg);
}
import { decycle } from './util';
export function action(name) {
// eslint-disable-next-line no-unused-vars, func-names
const handler = function(..._args) {
const args = Array.from(_args).map(_format);
const args = Array.from(_args).map(arg => JSON.stringify(decycle(arg)));
const channel = addons.getChannel();
const id = uuid();
channel.emit(EVENT_ID, {

View File

@ -23,5 +23,17 @@ describe('preview', () => {
expect(channel.emit.mock.calls[0][1].id).toBe('42');
expect(channel.emit.mock.calls[1][1].id).toBe('24');
});
it('should be able to handle cyclic object without hanging', () => {
const cyclicObject = {
propertyA: {
innerPropertyA: {},
},
propertyB: 'b',
};
cyclicObject.propertyA.innerPropertyA = cyclicObject;
expect(() => JSON.stringify(cyclicObject)).toThrow();
expect(() => action('foo')(cyclicObject)).not.toThrow();
});
});
});

129
addons/actions/src/util.js Normal file
View File

@ -0,0 +1,129 @@
export const CLASS_NAME_KEY = '$___storybook.className';
export const CYCLIC_KEY = '$___storybook.isCyclic';
export function muteProperty(key, value) {
return Object.defineProperty(value, key, { enumerable: false });
}
export function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
export function createFakeConstructor(obj) {
function FakeConstructor(data) {
Object.assign(this, data);
}
Object.defineProperty(FakeConstructor, 'name', {
value: obj[CLASS_NAME_KEY],
});
delete obj[CLASS_NAME_KEY]; // eslint-disable-line no-param-reassign
return new FakeConstructor(obj);
}
export function reviver(key, value) {
if (isObject(value) && value[CLASS_NAME_KEY]) {
return createFakeConstructor(value);
}
return value;
}
// Based on: https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
export function decycle(object, depth = 15) {
const objects = new WeakMap();
let isCyclic = false;
return (function derez(value, path, _depth) {
let oldPath;
let obj;
if (Object(value) === value && _depth > depth) {
return `[${value.constructor.name}...]`;
}
if (
typeof value === 'object' &&
value !== null &&
!(value instanceof Boolean) &&
!(value instanceof Date) &&
!(value instanceof Number) &&
!(value instanceof RegExp) &&
!(value instanceof String)
) {
oldPath = objects.get(value);
if (oldPath !== undefined) {
isCyclic = true;
return { $ref: oldPath };
}
objects.set(value, path);
if (Array.isArray(value)) {
obj = [];
for (let i = 0; i < value.length; i += 1) {
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1);
}
} else {
obj = { [CLASS_NAME_KEY]: value.constructor ? value.constructor.name : 'Object' };
Object.keys(value).forEach(name => {
obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1);
});
}
if (_depth === 0 && isObject(value) && isCyclic) {
obj[CYCLIC_KEY] = true;
}
return obj;
}
return value;
})(object, '$', 0);
}
export function retrocycle(json) {
const pathReg = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\([\\"/bfnrt]|u[0-9a-zA-Z]{4}))*")])*$/;
const $ = JSON.parse(json, reviver);
(function rez(value) {
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i += 1) {
const item = value[i];
if (item && typeof item === 'object') {
const path = item.$ref;
if (typeof path === 'string' && pathReg.test(path)) {
value[i] = eval(path); // eslint-disable-line no-eval, no-param-reassign
} else {
rez(item);
}
}
}
} else {
Object.keys(value).forEach(name => {
const item = value[name];
if (typeof item === 'object' && item !== null) {
const path = item.$ref;
if (typeof path === 'string' && pathReg.test(path)) {
value[name] = eval(path); // eslint-disable-line no-eval, no-param-reassign
} else {
rez(item);
}
}
});
}
}
})($);
muteProperty(CYCLIC_KEY, $);
return $;
}

View File

@ -1,6 +1,10 @@
{
"name": "@storybook/addon-centered",
<<<<<<< HEAD
"version": "3.2.16",
=======
"version": "3.3.0-alpha.2",
>>>>>>> d997c5724823ef51c644c6135f8ad619d893eb0a
"description": "Storybook decorator to center components",
"license": "MIT",
"author": "Muhammed Thanish <mnmtanish@gmail.com>",

View File

@ -1,7 +0,0 @@
// Use the line below to register this addon
// import '@storybook/addon-comments/register';
import '@storybook/addon-actions/register';
import '@kadira/storybook-database-cloud/register';
import { init } from '../src/manager';
init();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View File

@ -1,3 +0,0 @@
const preview = require('./dist/preview');
preview.init();

View File

@ -1,30 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
const buttonStyles = {
border: '1px solid #eee',
borderRadius: 3,
backgroundColor: '#FFFFFF',
cursor: 'pointer',
fontSize: 15,
padding: '3px 10px',
};
const Button = ({ children, onClick, style = {} }) => (
<button style={{ ...buttonStyles, ...style }} onClick={onClick}>
{children}
</button>
);
Button.defaultProps = {
onClick: () => {},
style: {},
};
Button.propTypes = {
children: PropTypes.string.isRequired,
onClick: PropTypes.func,
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
export default Button;

View File

@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { window } from 'global';
import Textarea from 'react-textarea-autosize';
import marked from 'marked';
import style from './style';
const renderer = new marked.Renderer();
renderer.heading = text => text;
marked.setOptions({
renderer,
gfm: true,
tables: false,
breaks: true,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false,
});
export default class CommentForm extends Component {
constructor(props, ...args) {
super(props, ...args);
this.state = { text: '' };
}
onChange(e) {
const text = e.target.value;
this.setState({ text });
}
onSubmit() {
const { addComment } = this.props;
const text = this.state.text.trim();
if (!text || text === '') {
return;
}
addComment(marked(text));
this.setState({ text: '' });
}
openLogin() {
const signInUrl = `https://hub.getstorybook.io/sign-in?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
window.location.href = signInUrl;
}
handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.onSubmit();
}
}
render() {
if (!this.props.user) {
return (
<div style={style.wrapper}>
<Textarea style={style.input} disabled />
<button style={style.submitButton} onClick={() => this.openLogin()}>
Sign in with Storybook Hub
</button>
</div>
);
}
const { text } = this.state;
return (
<div style={style.wrapper}>
<Textarea
style={style.input}
onChange={e => this.onChange(e)}
onKeyDown={e => this.handleKeyDown(e)}
placeholder="Add your comment..."
value={text}
/>
<button style={style.submitButton} onClick={() => this.onSubmit()}>
Submit
</button>
</div>
);
}
}
CommentForm.defaultProps = {
user: null,
addComment: () => {},
};
CommentForm.propTypes = {
user: PropTypes.object, // eslint-disable-line react/forbid-prop-types
addComment: PropTypes.func,
};

View File

@ -1,39 +0,0 @@
const button = {
boxSizing: 'border-box',
height: 30,
border: 'none',
outline: 'none',
background: '#fafafa',
padding: '7px 15px',
fontSize: 12,
lineHeight: 1,
color: 'rgba(0, 0, 0, 0.5)',
};
export default {
wrapper: {
display: 'flex',
alignItems: 'center',
borderTop: '1px solid rgb(234, 234, 234)',
},
submitButton: {
...button,
cursor: 'pointer',
borderRadius: '0 0 4px 0',
},
input: {
flex: 1,
boxSizing: 'border-box',
height: 30,
maxHeight: 70,
border: 'none',
borderRadius: '0 0 0 4px',
outline: 'none',
padding: '5px 10px',
fontSize: 13,
lineHeight: 1.6,
color: 'rgba(0, 0, 0, 0.8)',
fontFamily: 'sans-serif',
resize: 'none',
},
};

View File

@ -1,51 +0,0 @@
export default `
.comment-content p {
margin: 0;
padding: 0;
}
.comment-content pre {
white-space: pre-wrap;
word-wrap: break-word;
padding: 5px 8px;
color: #000;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.94em;
border-radius: 3px;
background-color: #F8F8F8;
border: 1px solid #f1f1f1;
}
.comment-content pre code {
border: 0px !important;
background: transparent !important;
line-height: 1.3em;
}
.comment-content code {
padding: 0 3px 0 3px;
color: #000;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.94em;
border-radius: 3px;
background-color: #F8F8F8;
border: 1px solid #f1f1f1;
}
.comment-content blockquote {
color: #666666;
margin: 3px 0;
padding-left: 12px;
border-left: 0.5em #EEE solid;
}
.comment-content ul, .comment-content ol {
margin: 1em 0;
padding: 0 0 0 2em;
}
.comment-content a {
color: #0645ad;
text-decoration: none;
}
`;

View File

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import style from './style';
import CommentItem from '../CommentItem';
export default class CommentList extends Component {
componentDidMount() {
this.wrapper.scrollTop = this.wrapper.scrollHeight;
}
componentDidUpdate(prev) {
if (this.props.comments.length !== prev.comments.length) {
this.wrapper.scrollTop = this.wrapper.scrollHeight;
}
}
render() {
const { comments } = this.props;
if (comments.length === 0) {
return (
<div
ref={el => {
this.wrapper = el;
}}
style={style.wrapper}
>
<div style={style.noComments}>No Comments Yet!</div>
</div>
);
}
return (
<div
ref={el => {
this.wrapper = el;
}}
style={style.wrapper}
>
{comments.map(comment => (
<CommentItem
key={comment.id}
comment={comment}
ownComment={comment.userId === (this.props.user && this.props.user.id)}
deleteComment={() => this.props.deleteComment(comment.id)}
/>
))}
</div>
);
}
}
CommentList.defaultProps = {
comments: [],
user: null,
deleteComment: () => {},
};
CommentList.propTypes = {
comments: PropTypes.arrayOf(PropTypes.object),
user: PropTypes.shape({
id: PropTypes.number,
}),
deleteComment: PropTypes.func,
};

View File

@ -1,12 +0,0 @@
export default {
wrapper: {
flex: 1,
overflow: 'auto',
padding: '7px 15px',
},
noComments: {
fontFamily: 'sans-serif',
fontSize: 13,
padding: '10px 0',
},
};

View File

@ -1,45 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import CommentList from '../CommentList';
import CommentForm from '../CommentForm';
import style from './style';
export default function CommentsPanel(props) {
if (props.loading) {
return (
<div style={style.wrapper}>
<div style={style.message}>loading...</div>
</div>
);
}
if (props.appNotAvailable) {
const appsUrl = 'https://hub.getstorybook.io/apps';
return (
<div style={style.wrapper}>
<div style={style.message}>
<a style={style.button} href={appsUrl}>
Create an app for this repo on Storybook Hub
</a>
</div>
</div>
);
}
return (
<div style={style.wrapper}>
<CommentList key="list" {...props} />
<CommentForm key="form" {...props} />
</div>
);
}
CommentsPanel.defaultProps = {
loading: false,
appNotAvailable: false,
};
CommentsPanel.propTypes = {
loading: PropTypes.bool,
appNotAvailable: PropTypes.bool,
};

View File

@ -1,29 +0,0 @@
export default {
wrapper: {
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
},
message: {
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'sans-serif',
color: 'rgb(68, 68, 68)',
fontSize: 11,
letterSpacing: 1,
textDecoration: 'none',
textTransform: 'uppercase',
},
button: {
textDecoration: 'none',
color: '#444',
padding: '10px 18px',
background: 'rgb(240, 240, 240)',
borderRadius: 5,
textTransform: 'none',
fontSize: 12,
},
};

View File

@ -1,236 +0,0 @@
import deepEquals from 'deep-equal';
import { EventEmitter } from 'events';
export default class DataStore {
constructor(db) {
this.db = db;
this.currentStory = null;
this.callbacks = [];
this.cache = {};
this.users = {};
this.user = null;
this.eventStore = new EventEmitter();
}
addToCache(currentStory, comments) {
const key = this.getStoryKey(currentStory);
this.cache[key] = {
comments,
addedAt: Date.now(),
};
}
getFromCache(currentStory) {
const key = this.getStoryKey(currentStory);
const item = this.cache[key];
if (!item) {
return {
comments: [],
invalidated: false,
};
}
const { comments } = item;
let invalidated = false;
// invalid caches created 60 minutes ago.
if (Date.now() - item.addedAt > 1000 * 60) {
delete this.cache[key];
invalidated = true;
}
return { comments, invalidated };
}
reloadCurrentComments() {
if (this.stopReloading) {
clearInterval(this.stopReloading);
}
this.stopReloading = setInterval(
() => {
this.loadUsers().then(() => this.loadComments());
},
1000 * 60 // Reload for every minute
);
}
setCurrentStory(sbKind, sbStory) {
this.currentStory = { sbKind, sbStory };
// We don't need to do anything if the there's no loggedIn user.
// if (!this.user) return;
this.reloadCurrentComments();
const item = this.getFromCache(this.currentStory);
if (item) {
this.fireComments(item.comments);
// if the cache invalidated we need to load comments again.
if (item.invalidated) {
return this.loadUsers().then(() => this.loadComments());
}
return Promise.resolve(null);
}
// load comments for the first time.
// TODO: send a null and handle the loading part in the UI side.
this.eventStore.emit('loading', true);
this.fireComments([]);
this.loadUsers()
.then(() => this.loadComments())
.then(() => {
this.eventStore.emit('loading', false);
return Promise.resolve(null);
});
return this.currentStory;
}
setCurrentUser(user) {
this.user = user;
}
loadUsers() {
const query = {};
const options = { limit: 1e6 };
return this.db.persister.getAppInfo().then(info => {
if (!info) {
return null;
}
return this.db
.getCollection('users')
.get(query, options)
.then(users => {
this.users = users.reduce((newUsers, user) => {
const usersObj = {
...newUsers,
};
usersObj[user.id] = user;
return usersObj;
}, {});
});
});
}
loadComments() {
const currentStory = { ...this.currentStory };
const query = currentStory;
const options = { limit: 1e6 };
return this.db.persister.getAppInfo().then(info => {
if (!info) {
return null;
}
return this.db
.getCollection('comments')
.get(query, options)
.then(comments => {
// add to cache
this.addToCache(currentStory, comments);
// set comments only if we are on the relavant story
if (deepEquals(currentStory, this.currentStory)) {
this.fireComments(comments);
}
});
});
}
getStoryKey(currentStory) {
return `${currentStory.sbKind}:::${currentStory.sbStory}`;
}
fireComments(comments) {
this.callbacks.forEach(callback => {
// link user to the comment directly
const commentsWithUser = comments.map(comment =>
Object.assign({}, comment, { user: this.users[comment.userId] })
);
callback(commentsWithUser);
});
}
onComments(cb) {
this.callbacks.push(cb);
const stop = () => {
const index = this.callbacks.indexOf(cb);
this.callbacks.splice(index, 1);
};
return stop;
}
addPendingComment(comment) {
// Add the pending comment.
const pendingComment = { ...comment, loading: true };
const { comments: existingComments } = this.getFromCache(this.currentStory);
const updatedComments = existingComments.concat(pendingComment);
this.fireComments(updatedComments);
return Promise.resolve(null);
}
setDeletedComment(commentId) {
const { comments } = this.getFromCache(this.currentStory);
const deleted = comments.find(c => c.id === commentId);
if (deleted) {
deleted.loading = true;
}
this.fireComments(comments);
return Promise.resolve(null);
}
addAuthorToTheDatabase() {
if (this.users[this.user.id]) {
// user exists in the DB.
return Promise.resolve(null);
}
// add user to the local cache
this.users[this.user.id] = this.user;
// add user to the actual collection
return this.db.getCollection('users').set(this.user);
}
// NOTE the "sbProtected" makes sure only the author can modify
// or delete a comment after its saved on the cloud database.
addCommentToDatabase(comment) {
const doc = {
...comment,
...this.currentStory,
...this.currentStory,
sbProtected: true,
};
return this.db.getCollection('comments').set(doc);
}
deleteCommentOnDatabase(commentId) {
const query = { id: commentId };
return this.db.getCollection('comments').del(query);
}
addComment(comment) {
return this.addAuthorToTheDatabase()
.then(() => this.addPendingComment(comment))
.then(() => this.addCommentToDatabase(comment))
.then(() => this.loadUsers())
.then(() => this.loadComments());
}
deleteComment(commentId) {
return this.setDeletedComment(commentId)
.then(() => this.deleteCommentOnDatabase(commentId))
.then(() => this.loadComments());
}
onLoading(cb) {
this.eventStore.on('loading', cb);
return () => {
this.eventStore.removeListener('loading', cb);
};
}
}

View File

@ -1,92 +0,0 @@
import DataStore from './dataStore';
const user = {
id: 'user-id',
name: 'user-name',
};
const dbGetUsers = jest.fn(() => Promise.resolve([user]));
const dbSetUsers = jest.fn(a => Promise.resolve(a));
const dbGetComments = jest.fn(a => Promise.resolve(a));
const dbSetComments = jest.fn(a => Promise.resolve(a));
const db = {
getCollection(namespace) {
switch (namespace) {
case 'users': {
return {
get: dbGetUsers,
set: dbSetUsers,
};
}
case 'comments': {
return {
get: dbGetComments,
set: dbSetComments,
};
}
default: {
return {};
}
}
},
persister: {
getAppInfo() {
return Promise.resolve(true);
},
},
};
const theStore = new DataStore(db);
describe('DataStore', () => {
it('set current story - when user not logged in', () => {
theStore.setCurrentStory('Components', 'CommentList - No Comments');
expect(theStore.currentStory).toEqual({
sbKind: 'Components',
sbStory: 'CommentList - No Comments',
});
});
it('set current user', () => {
theStore.setCurrentUser({
id: 'user-id',
name: 'user-name',
});
expect(theStore.user).toEqual({ id: 'user-id', name: 'user-name' });
});
it('set current story - when user already logged in', () => {
theStore.setCurrentStory('Components', 'CommentList - No Comments');
expect(theStore.currentStory).toEqual({
sbKind: 'Components',
sbStory: 'CommentList - No Comments',
});
});
it('add comment', async () => {
theStore.setCurrentStory('Components', 'CommentList - No Comments');
const comment = {
text: 'sample comment',
time: 1476435982029,
userId: 'user-id',
};
await theStore.addComment(comment);
expect(dbGetComments).toHaveBeenCalled();
expect(dbSetComments).toHaveBeenCalled();
});
it('onComments', () => {
const callback = comments => comments;
const stopper = theStore.onComments(callback);
expect(theStore.callbacks).toContain(callback);
expect(stopper).not.toThrow();
expect(theStore.callbacks).not.toContain(callback);
});
});

View File

@ -1,113 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import addons from '@storybook/addons';
import CommentsPanel from '../../components/CommentsPanel/';
import DataStore from './dataStore';
export default class Container extends Component {
constructor(props, ...args) {
super(props, ...args);
this.state = {
user: null,
comments: [],
loading: true,
};
}
componentDidMount() {
const db = addons.getDatabase();
this.store = new DataStore(db);
this.stopListeningToComments = this.store.onComments(comments => {
this.setState({ comments });
});
// Clear the current notes on every story change.
this.stopListeningOnStory = this.props.api.onStory((kind, story) => {
// set the current selection
this.store.setCurrentStory(kind, story);
});
this.stopListingStoreLoading = this.store.onLoading(loading => {
this.setState({ loading });
});
this.init();
}
componentWillUnmount() {
this.stopListeningToComments();
this.stopListeningOnStory();
this.stopListingStoreLoading();
}
getAppInfo(persister) {
return persister
.getAppInfo()
.then(appInfo => Promise.resolve(appInfo), () => Promise.resolve(null));
}
init() {
const db = addons.getDatabase();
if (typeof db.persister.getUser !== 'function') {
throw new Error('unable to get user info');
}
this.setState({ loading: true });
db.persister
.getUser()
.then(u => Promise.resolve(u), () => Promise.resolve(null))
.then(user => {
if (user) {
this.store.setCurrentUser(user);
this.setState({ user });
} else {
this.setState({ user: null });
}
return this.getAppInfo(db.persister);
})
.then(appInfo => {
const updatedState = { loading: false };
if (!appInfo) {
updatedState.appNotAvailable = true;
}
this.setState(updatedState);
});
}
addComment(text) {
const time = Date.now();
const { user } = this.state;
const comment = {
text,
time,
userId: user.id,
};
this.store.addComment(comment);
}
deleteComment(commentId) {
this.store.deleteComment(commentId);
}
render() {
const props = {
user: this.state.user,
comments: this.state.comments,
loading: this.state.loading,
appNotAvailable: this.state.appNotAvailable,
deleteComment: commentId => this.deleteComment(commentId),
addComment: text => this.addComment(text),
};
return <CommentsPanel {...props} />;
}
}
Container.propTypes = {
api: PropTypes.shape({
onStory: PropTypes.func.isRequired,
}).isRequired,
};

View File

@ -1,16 +0,0 @@
/* eslint import/prefer-default-export:0 */
import React from 'react';
import addons from '@storybook/addons';
import CommentsPanel from './containers/CommentsPanel';
import { ADDON_ID, PANEL_ID } from '../shared';
export function init() {
addons.register(ADDON_ID, api => {
// add 'Comments' panel
addons.addPanel(PANEL_ID, {
title: 'Comments',
render: () => <CommentsPanel api={api} />,
});
});
}

View File

@ -1 +0,0 @@
export const init = () => {};

View File

@ -1,3 +0,0 @@
// addons, panels and events get unique names using a prefix
export const ADDON_ID = 'storybooks/addon-comments';
export const PANEL_ID = `${ADDON_ID}/comments-panel`;

View File

@ -1,85 +0,0 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Button from '../index';
import CommentForm from '../manager/components/CommentForm';
import CommentList from '../manager/components/CommentList';
import CommentsPanel from '../manager/components/CommentsPanel';
const userObj = {
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
name: 'User A',
};
const commentsList = [
{
loading: false,
user: {
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
name: 'User A',
},
time: 'Wed Oct 12 2016 13:36:59 GMT+0530 (IST)',
text: 'Lorem ipsum dolor sit amet, <pre><code>Ut odio massa, rutrum et purus id.</code></pre>',
},
{
loading: false,
user: {
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
name: 'User B',
},
time: 'Wed Oct 12 2016 13:38:46 GMT+0530 (IST)',
text:
'Vivamus tortor nisi, <b>efficitur</b> in rutrum <em>ac</em>, tempor <code>et mauris</code>. In et rutrum enim',
},
{
loading: true,
user: {
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
name: 'User C',
},
time: 'Wed Oct 12 2016 13:38:55 GMT+0530 (IST)',
text: 'sample comment 3',
},
];
storiesOf('Button', module)
.add('default view', () => <Button onClick={action('button clicked')}>Hello</Button>)
.add('some emojies as the text', () => <Button>😀 😎 👍 💯</Button>)
.add('custom styles', () => {
const style = {
fontSize: 20,
textTransform: 'uppercase',
color: '#FF8833',
};
return <Button style={style}>Hello</Button>;
});
storiesOf('Components', module)
.add('CommentForm', () => <CommentForm addComment={action('addComment')} />)
.add('CommentList - No Comments', () => <CommentList comments={[]} />)
.add('CommentList - with comments', () => (
<CommentList user={userObj} comments={commentsList} deleteComment={action('deleteComment')} />
))
.add('CommentPanel - not loggedIn', () => <CommentsPanel />)
.add('CommentPanel - app not available', () => (
<CommentsPanel user={userObj} appNotAvailable={{}} />
))
.add('CommentPanel - loggedIn with no comments', () => (
<CommentsPanel
user={userObj}
loading={false}
comments={[]}
addComment={action('addComment')}
deleteComment={action('deleteComment')}
/>
))
.add('CommentPanel - loggedIn with has comments', () => (
<CommentsPanel
user={userObj}
loading={false}
comments={commentsList}
addComment={action('addComment')}
deleteComment={action('deleteComment')}
/>
));

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-events",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Add events to your Storybook stories.",
"keywords": [
"addon",
@ -20,7 +20,7 @@
"storybook": "start-storybook -p 6006"
},
"dependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/addons": "^3.3.0-alpha.2",
"babel-runtime": "^6.26.0",
"format-json": "^1.0.3",
"prop-types": "^15.6.0",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-graphql",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Storybook addon to display the GraphiQL IDE",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-info",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "A Storybook addon to show additional information for your stories.",
"license": "MIT",
"main": "dist/index.js",
@ -14,8 +14,8 @@
"storybook": "start-storybook -p 9010"
},
"dependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/components": "^3.2.16",
"@storybook/addons": "^3.3.0-alpha.2",
"@storybook/components": "^3.3.0-alpha.2",
"babel-runtime": "^6.26.0",
"global": "^4.3.2",
"marksy": "^2.0.0",

View File

@ -2,7 +2,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Table, Td, Th } from '@storybook/components';
import PropVal from './PropVal';
import PrettyPropType from './types/PrettyPropType';
const PropTypesMap = new Map();
@ -13,41 +16,10 @@ Object.keys(PropTypes).forEach(typeName => {
PropTypesMap.set(type.isRequired, typeName);
});
const stylesheet = {
propTable: {
marginLeft: -10,
borderSpacing: '10px 5px',
borderCollapse: 'separate',
},
};
const isNotEmpty = obj => obj && obj.props && Object.keys(obj.props).length > 0;
const renderDocgenPropType = propType => {
if (!propType) {
return 'unknown';
}
const { name } = propType;
switch (name) {
case 'arrayOf':
return `${propType.value.name}[]`;
case 'instanceOf':
return propType.value;
case 'union':
return propType.raw;
case 'signature':
return propType.raw;
default:
return name;
}
};
const hasDocgen = type => isNotEmpty(type.__docgenInfo);
const boolToString = value => (value ? 'yes' : 'no');
const propsFromDocgen = type => {
const props = {};
const docgenInfoProps = type.__docgenInfo.props;
@ -59,8 +31,8 @@ const propsFromDocgen = type => {
props[property] = {
property,
propType: renderDocgenPropType(propType),
required: boolToString(docgenInfoProp.required),
propType,
required: docgenInfoProp.required,
description: docgenInfoProp.description,
defaultValue: defaultValueDesc.value,
};
@ -75,21 +47,15 @@ const propsFromPropTypes = type => {
if (type.propTypes) {
Object.keys(type.propTypes).forEach(property => {
const typeInfo = type.propTypes[property];
const required = boolToString(typeInfo.isRequired === undefined);
const description =
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property]
? type.__docgenInfo.props[property].description
: null;
const required = typeInfo.isRequired === undefined;
const docgenInfo =
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property];
const description = docgenInfo ? docgenInfo.description : null;
let propType = PropTypesMap.get(typeInfo) || 'other';
if (propType === 'other') {
if (
type.__docgenInfo &&
type.__docgenInfo.props &&
type.__docgenInfo.props[property] &&
type.__docgenInfo.props[property].type
) {
propType = type.__docgenInfo.props[property].type.name;
if (docgenInfo && docgenInfo.type) {
propType = docgenInfo.type.name;
}
}
@ -116,6 +82,23 @@ const propsFromPropTypes = type => {
return props;
};
export const multiLineText = input => {
if (!input) return input;
const text = String(input);
const arrayOfText = text.split(/\r?\n|\r/g);
const isSingleLine = arrayOfText.length < 2;
return isSingleLine
? text
: arrayOfText.map((
lineOfText,
i // note: lineOfText is the closest we will get to a unique key
) => (
<span key={lineOfText}>
{i > 0 && <br />} {lineOfText}
</span>
));
};
export default function PropTable(props) {
const { type, maxPropObjectKeys, maxPropArrayLength, maxPropStringLength } = props;
@ -137,34 +120,38 @@ export default function PropTable(props) {
};
return (
<table style={stylesheet.propTable}>
<Table>
<thead>
<tr>
<th>property</th>
<th>propType</th>
<th>required</th>
<th>default</th>
<th>description</th>
<Th bordered>property</Th>
<Th bordered>propType</Th>
<Th bordered>required</Th>
<Th bordered>default</Th>
<Th bordered>description</Th>
</tr>
</thead>
<tbody>
{array.map(row => (
<tr key={row.property}>
<td>{row.property}</td>
<td>{row.propType}</td>
<td>{row.required}</td>
<td>
<Td bordered code>
{row.property}
</Td>
<Td bordered code>
<PrettyPropType propType={row.propType} />
</Td>
<Td bordered>{row.required ? 'yes' : '-'}</Td>
<Td bordered>
{row.defaultValue === undefined ? (
'-'
) : (
<PropVal val={row.defaultValue} {...propValProps} />
)}
</td>
<td>{row.description}</td>
</Td>
<Td bordered>{multiLineText(row.description)}</Td>
</tr>
))}
</tbody>
</table>
</Table>
);
}

View File

@ -0,0 +1,34 @@
import renderer from 'react-test-renderer';
import { multiLineText } from './PropTable';
describe('PropTable', () => {
describe('multiLineText', () => {
const singleLine = 'Foo bar baz';
const unixMultiLineText = 'foo \n bar \n baz';
const windowsMultiLineText = 'foo \r bar \r baz';
it('should return a blank string for a null input', () => {
expect(multiLineText(null)).toBe(null);
});
it('should return a blank string for an undefined input', () => {
expect(multiLineText(undefined)).toBe(undefined);
});
it('should cast a number to a string', () => {
expect(multiLineText(1)).toBe('1');
});
it('should return its input for a single line of text', () => {
expect(multiLineText(singleLine)).toBe(singleLine);
});
it('should return an array for unix multiline text', () => {
expect(multiLineText(unixMultiLineText)).toHaveLength(3);
});
it('should return an array for windows multiline text', () => {
expect(multiLineText(windowsMultiLineText)).toHaveLength(3);
});
it('should have 2 br tags for 3 lines of text', () => {
const tree = renderer.create(multiLineText(unixMultiLineText)).toJSON();
expect(tree).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PropTable multiLineText should have 2 br tags for 3 lines of text 1`] = `
Array [
<span>
foo
</span>,
<span>
<br />
bar
</span>,
<span>
<br />
baz
</span>,
]
`;

View File

@ -0,0 +1,6 @@
// TODO remove this file once https://github.com/yannickcr/eslint-plugin-react/issues/1389 is solved
const warn = 1;
module.exports = {
rules: { 'react/no-typos': warn },
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import PrettyPropType from './PrettyPropType';
import { TypeInfo } from './proptypes';
const ArrayOf = ({ propType }) => (
<span>
<span>[</span>
<span>
<PrettyPropType propType={propType.value} />
</span>
<span>]</span>
</span>
);
ArrayOf.propTypes = {
propType: TypeInfo.isRequired,
};
export default ArrayOf;

View File

@ -0,0 +1,8 @@
import React from 'react';
import { TypeInfo } from './proptypes';
const Enum = ({ propType }) => <span>{propType.value.map(({ value }) => value).join(' | ')}</span>;
Enum.propTypes = {
propType: TypeInfo.isRequired,
};

View File

@ -0,0 +1,10 @@
import React from 'react';
import { TypeInfo } from './proptypes';
const InstanceOf = ({ propType }) => <span>{propType.value}</span>;
InstanceOf.propTypes = {
propType: TypeInfo.isRequired,
};
export default InstanceOf;

View File

@ -0,0 +1,5 @@
import React from 'react';
const ObjectType = () => <span>{}</span>;
export default ObjectType;

View File

@ -0,0 +1,18 @@
import React from 'react';
import PrettyPropType from './PrettyPropType';
import { TypeInfo } from './proptypes';
const ObjectOf = ({ propType }) => (
<span>
{'{[<key>]: '}
<PrettyPropType propType={propType.value} />
{'}'}
</span>
);
ObjectOf.propTypes = {
propType: TypeInfo.isRequired,
};
export default ObjectOf;

View File

@ -0,0 +1,5 @@
import React from 'react';
const ObjectType = () => <span>{}</span>;
export default ObjectType;

View File

@ -0,0 +1,10 @@
import React from 'react';
import { TypeInfo } from './proptypes';
const OneOf = ({ propType }) => <span>{propType.value.map(({ value }) => value).join(' | ')}</span>;
OneOf.propTypes = {
propType: TypeInfo.isRequired,
};
export default OneOf;

View File

@ -0,0 +1,25 @@
import React from 'react';
import PrettyPropType from './PrettyPropType';
import { TypeInfo } from './proptypes';
const OneOfType = ({ propType }) => {
const { length } = propType.value;
return (
<span>
{propType.value
.map((value, i) => [
<PrettyPropType
key={`${value.name}${value.value ? `-${value.value}` : ''}`}
propType={value}
/>,
i < length - 1 ? <span> | </span> : null,
])
.reduce((acc, tuple) => acc.concat(tuple), [])}
</span>
);
};
OneOfType.propTypes = {
propType: TypeInfo.isRequired,
};
export default OneOfType;

View File

@ -0,0 +1,57 @@
import PropTypes from 'prop-types';
import React from 'react';
import ObjectType from './ObjectType';
import Shape from './Shape';
import OneOfType from './OneOfType';
import ArrayOf from './ArrayOf';
import ObjectOf from './ObjectOf';
import OneOf from './OneOf';
import InstanceOf from './InstanceOf';
import Signature from './Signature';
import { TypeInfo } from './proptypes';
// propType -> Component map - these are a bit more complex prop types to display
const propTypeComponentMap = new Map([
['shape', Shape],
['union', OneOfType],
['arrayOf', ArrayOf],
['objectOf', ObjectOf],
// Might be overkill to have below proptypes as separate components *shrug*
['object', ObjectType],
['enum', OneOf],
['instanceOf', InstanceOf],
['signature', Signature],
]);
const PrettyPropType = props => {
const { propType, depth } = props;
if (!propType) {
return <span>unknown</span>;
}
const { name } = propType || {};
if (propTypeComponentMap.has(name)) {
const Component = propTypeComponentMap.get(name);
return <Component propType={propType} depth={depth} />;
}
// Otherwise, propType does not have a dedicated component, display proptype name by default
return <span>{name}</span>;
};
PrettyPropType.displayName = 'PrettyPropType';
PrettyPropType.defaultProps = {
propType: null,
depth: 1,
};
PrettyPropType.propTypes = {
propType: TypeInfo,
depth: PropTypes.number,
};
export default PrettyPropType;

View File

@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
const styles = {
hasProperty: {
whiteSpace: 'nowrap',
},
};
const PropertyLabel = ({ property, required }) => {
if (!property) return null;
return (
<span style={styles.hasProperty}>
{property}
{required ? '' : '?'}:{' '}
</span>
);
};
PropertyLabel.propTypes = {
property: PropTypes.string,
required: PropTypes.bool,
};
PropertyLabel.defaultProps = {
property: '',
required: false,
};
export default PropertyLabel;

View File

@ -0,0 +1,81 @@
import PropTypes from 'prop-types';
import React from 'react';
import { HighlightButton } from '@storybook/components';
import PrettyPropType from './PrettyPropType';
import PropertyLabel from './PropertyLabel';
import { TypeInfo } from './proptypes';
const MARGIN_SIZE = 15;
class Shape extends React.Component {
constructor(props) {
super(props);
this.state = {
minimized: false,
};
}
handleToggle = () => {
this.setState({
minimized: !this.state.minimized,
});
};
handleMouseEnter = () => {
this.setState({ hover: true });
};
handleMouseLeave = () => {
this.setState({ hover: false });
};
render() {
const { propType, depth } = this.props;
return (
<span>
<HighlightButton
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
highlight={this.state.hover}
onClick={this.handleToggle}
>
{'{'}
</HighlightButton>
<HighlightButton onClick={this.handleToggle}>...</HighlightButton>
{!this.state.minimized &&
Object.keys(propType.value).map(childProperty => (
<div key={childProperty} style={{ marginLeft: depth * MARGIN_SIZE }}>
<PropertyLabel
property={childProperty}
required={propType.value[childProperty].required}
/>
<PrettyPropType depth={depth + 1} propType={propType.value[childProperty]} />
,
</div>
))}
<HighlightButton
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
highlight={this.state.hover}
onClick={this.handleToggle}
>
{'}'}
</HighlightButton>
</span>
);
}
}
Shape.propTypes = {
propType: TypeInfo,
depth: PropTypes.number.isRequired,
};
Shape.defaultProps = {
propType: null,
};
export default Shape;

View File

@ -0,0 +1,10 @@
import React from 'react';
import { TypeInfo } from './proptypes';
const Signature = ({ propType }) => <span>{propType.raw}</span>;
Signature.propTypes = {
propType: TypeInfo.isRequired,
};
export default Signature;

View File

@ -0,0 +1,6 @@
import PropTypes from 'prop-types';
export const TypeInfo = PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
});

View File

@ -7,6 +7,11 @@
[![Storybook Slack](https://now-examples-slackin-nqnzoygycp.now.sh/badge.svg)](https://now-examples-slackin-nqnzoygycp.now.sh/)
[![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors)
This addon works with Storybook for:
[React](https://github.com/storybooks/storybook/tree/master/app/react).
[React Native](https://github.com/storybooks/storybook/tree/master/app/react-native).
[Vue](https://github.com/storybooks/storybook/tree/master/app/vue).
* * *
Storybook Addon Knobs allow you to edit React props dynamically using the Storybook UI.
@ -41,7 +46,7 @@ Now, write your stories with knobs.
```js
import { storiesOf } from '@storybook/react';
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs';
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/react';
const stories = storiesOf('Storybook Knobs', module);
@ -54,7 +59,7 @@ stories.add('with a button', () => (
<button disabled={boolean('Disabled', false)} >
{text('Label', 'Hello Button')}
</button>
))
));
// Knobs as dynamic variables.
stories.add('as dynamic variables', () => {
@ -66,6 +71,27 @@ stories.add('as dynamic variables', () => {
});
```
> In the case of Vue, use these imports:
>
> ```js
> import { storiesOf } from '@storybook/vue';
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/vue';
> ```
>
> In the case of React-Native, use these imports:
>
> ```js
> import { storiesOf } from '@storybook/react-native';
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/react';
> ```
>
> In the case of Angular, use these imports:
>
> ```js
> import { storiesOf } from '@storybook/angular';
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/angular';
> ```
You can see your Knobs in a Storybook panel as shown below.
![](docs/demo.png)
@ -93,7 +119,7 @@ Just like that, you can import any other following Knobs:
Allows you to get some text from the user.
```js
import { text } from '@storybook/addon-knobs';
import { text } from '@storybook/addon-knobs/react';
const label = 'Your Name';
const defaultValue = 'Arunoda Susiripala';
@ -106,7 +132,7 @@ const value = text(label, defaultValue);
Allows you to get a boolean value from the user.
```js
import { boolean } from '@storybook/addon-knobs';
import { boolean } from '@storybook/addon-knobs/react';
const label = 'Agree?';
const defaultValue = false;
@ -119,7 +145,7 @@ const value = boolean(label, defaultValue);
Allows you to get a number from the user.
```js
import { number } from '@storybook/addon-knobs';
import { number } from '@storybook/addon-knobs/react';
const label = 'Age';
const defaultValue = 78;
@ -132,7 +158,7 @@ const value = number(label, defaultValue);
Allows you to get a number from the user using a range slider.
```js
import { number } from '@storybook/addon-knobs';
import { number } from '@storybook/addon-knobs/react';
const label = 'Temperature';
const defaultValue = 73;
@ -151,7 +177,7 @@ const value = number(label, defaultValue, options);
Allows you to get a colour from the user.
```js
import { color } from '@storybook/addon-knobs';
import { color } from '@storybook/addon-knobs/react';
const label = 'Color';
const defaultValue = '#ff00ff';
@ -164,7 +190,7 @@ const value = color(label, defaultValue);
Allows you to get a JSON object or array from the user.
```js
import { object } from '@storybook/addon-knobs';
import { object } from '@storybook/addon-knobs/react';
const label = 'Styles';
const defaultValue = {
@ -181,7 +207,7 @@ const value = object(label, defaultValue);
Allows you to get an array of strings from the user.
```js
import { array } from '@storybook/addon-knobs';
import { array } from '@storybook/addon-knobs/react';
const label = 'Styles';
const defaultValue = ['Red']
@ -193,7 +219,7 @@ const value = array(label, defaultValue);
> By default it's a comma, but this can be override by passing a separator variable.
>
> ```js
> import { array } from '@storybook/addon-knobs';
> import { array } from '@storybook/addon-knobs/react';
>
> const label = 'Styles';
> const defaultValue = ['Red'];
@ -206,7 +232,7 @@ const value = array(label, defaultValue);
Allows you to get a value from a select box from the user.
```js
import { select } from '@storybook/addon-knobs';
import { select } from '@storybook/addon-knobs/react';
const label = 'Colors';
const options = {
@ -226,7 +252,7 @@ const value = select(label, options, defaultValue);
Allow you to get date (and time) from the user.
```js
import { date } from '@storybook/addon-knobs';
import { date } from '@storybook/addon-knobs/react';
const label = 'Event Date';
const defaultValue = new Date('Jan 20 2017');
@ -263,7 +289,11 @@ If you feel like this addon is not performing well enough there is an option to
Usage:
```js
story.addDecorator(withKnobsOptions({
import { storiesOf } from '@storybook/react';
const stories = storiesOf('Storybook Knobs', module);
stories.addDecorator(withKnobsOptions({
debounce: { wait: number, leading: boolean}, // Same as lodash debounce.
timestamps: true // Doesn't emit events while user is typing.
}));

1
addons/knobs/angular.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/angular');

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-knobs",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Storybook Addon Prop Editor Component",
"license": "MIT",
"main": "dist/index.js",
@ -14,7 +14,8 @@
"storybook": "start-storybook -p 9010"
},
"dependencies": {
"@storybook/addons": "^3.2.16",
"@angular/core": "^5.0.0-beta.7",
"@storybook/addons": "^3.3.0-alpha.2",
"babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1",
"global": "^4.3.2",
@ -28,13 +29,10 @@
"util-deprecate": "^1.0.2"
},
"devDependencies": {
"@types/node": "^8.0.53",
"@types/react": "^16.0.25",
"git-url-parse": "^6.2.2",
"raw-loader": "^0.5.1",
"style-loader": "^0.19.0",
"typescript": "^2.6.1",
"typescript-definition-tester": "^0.0.5",
"vue": "^2.5.6"
"vue": "^2.5.2"
},
"peerDependencies": {
"react": "*",

1
addons/knobs/react.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/react');

117
addons/knobs/src/angular/helpers.js vendored Normal file
View File

@ -0,0 +1,117 @@
/* eslint no-underscore-dangle: 0 */
import { Component, SimpleChange, ChangeDetectorRef } from '@angular/core';
const getComponentMetadata = ({ component, props = {} }) => {
if (!component || typeof component !== 'function') throw new Error('No valid component provided');
const componentMeta = component.__annotations__[0] || component.annotations[0];
const propsMeta = component.__prop__metadata__ || component.propMetadata || {};
const paramsMetadata = component.__parameters__ || component.parameters || [];
return {
component,
props,
componentMeta,
propsMeta,
params: paramsMetadata,
};
};
const getAnnotatedComponent = ({ componentMeta, component, params, knobStore, channel }) => {
const NewComponent = function NewComponent(cd, ...args) {
component.call(this, ...args);
this.cd = cd;
this.knobChanged = this.knobChanged.bind(this);
this.setPaneKnobs = this.setPaneKnobs.bind(this);
};
NewComponent.prototype = Object.create(component.prototype);
NewComponent.__annotations__ = [new Component(componentMeta)];
NewComponent.__parameters__ = [[ChangeDetectorRef], ...params];
NewComponent.prototype.constructor = NewComponent;
NewComponent.prototype.ngOnInit = function onInit() {
if (component.prototype.ngOnInit) {
component.prototype.ngOnInit();
}
channel.on('addon:knobs:knobChange', this.knobChanged);
channel.on('addon:knobs:knobClick', this.knobClicked);
knobStore.subscribe(this.setPaneKnobs);
this.setPaneKnobs();
};
NewComponent.prototype.ngOnDestroy = function onDestroy() {
if (component.prototype.ngOnDestroy) {
component.prototype.ngOnDestroy();
}
channel.removeListener('addon:knobs:knobChange', this.knobChanged);
channel.removeListener('addon:knobs:knobClick', this.knobClicked);
knobStore.unsubscribe(this.setPaneKnobs);
};
NewComponent.prototype.ngOnChanges = function onChanges(changes) {
if (component.prototype.ngOnChanges) {
component.prototype.ngOnChanges(changes);
}
};
NewComponent.prototype.setPaneKnobs = function setPaneKnobs(timestamp = +new Date()) {
channel.emit('addon:knobs:setKnobs', {
knobs: knobStore.getAll(),
timestamp,
});
};
NewComponent.prototype.knobChanged = function knobChanged(change) {
const { name, value } = change;
const knobOptions = knobStore.get(name);
const oldValue = knobOptions.value;
knobOptions.value = value;
knobStore.markAllUnused();
const lowercasedName = name.toLocaleLowerCase();
this[lowercasedName] = value;
this.cd.detectChanges();
this.ngOnChanges({
[lowercasedName]: new SimpleChange(oldValue, value, false),
});
};
NewComponent.prototype.knobClicked = function knobClicked(clicked) {
const knobOptions = knobStore.get(clicked.name);
knobOptions.callback();
};
return NewComponent;
};
const resetKnobs = (knobStore, channel) => {
knobStore.reset();
channel.emit('addon:knobs:setKnobs', {
knobs: knobStore.getAll(),
timestamp: false,
});
};
export function prepareComponent({ getStory, context, channel, knobStore }) {
resetKnobs(knobStore, channel);
const { component, componentMeta, props, propsMeta, params } = getComponentMetadata(
getStory(context)
);
if (!componentMeta) throw new Error('No component metadata available');
const AnnotatedComponent = getAnnotatedComponent({
componentMeta,
component,
params,
knobStore,
channel,
});
return {
component: AnnotatedComponent,
props,
propsMeta,
};
}

39
addons/knobs/src/angular/index.js vendored Normal file
View File

@ -0,0 +1,39 @@
import addons from '@storybook/addons';
import { prepareComponent } from './helpers';
import {
knob,
text,
boolean,
number,
color,
object,
array,
date,
select,
button,
manager,
} from '../base';
export { knob, text, boolean, number, color, object, array, date, select, button };
export const angularHandler = (channel, knobStore) => getStory => context =>
prepareComponent({ getStory, context, channel, knobStore });
function wrapperKnobs(options) {
const channel = addons.getChannel();
manager.setChannel(channel);
if (options) channel.emit('addon:knobs:setOptions', options);
return angularHandler(channel, manager.knobStore);
}
export function withKnobs(storyFn, context) {
return wrapperKnobs()(storyFn)(context);
}
export function withKnobsOptions(options = {}) {
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
}

59
addons/knobs/src/base.js Normal file
View File

@ -0,0 +1,59 @@
import KnobManager from './KnobManager';
export const manager = new KnobManager();
export function knob(name, options) {
return manager.knob(name, options);
}
export function text(name, value) {
return manager.knob(name, { type: 'text', value });
}
export function boolean(name, value) {
return manager.knob(name, { type: 'boolean', value });
}
export function number(name, value, options = {}) {
const defaults = {
range: false,
min: 0,
max: 10,
step: 1,
};
const mergedOptions = { ...defaults, ...options };
const finalOptions = {
...mergedOptions,
type: 'number',
value,
};
return manager.knob(name, finalOptions);
}
export function color(name, value) {
return manager.knob(name, { type: 'color', value });
}
export function object(name, value) {
return manager.knob(name, { type: 'object', value });
}
export function select(name, options, value) {
return manager.knob(name, { type: 'select', options, value });
}
export function array(name, value, separator = ',') {
return manager.knob(name, { type: 'array', value, separator });
}
export function date(name, value = new Date()) {
const proxyValue = value ? value.getTime() : null;
return manager.knob(name, { type: 'date', value: proxyValue });
}
export function button(name, callback) {
return manager.knob(name, { type: 'button', callback, hideLabel: true });
}

View File

@ -16,18 +16,16 @@ describe('Array', () => {
expect(onChange).toHaveBeenCalledWith(['Fhishing', 'Skiing', 'Dancing']);
});
it('deserializes an Array to an Array', () => {
const array = ['a', 'b', 'c'];
const deserialized = ArrayType.deserialize(array);
it('should change to an empty array when emptied', () => {
const onChange = jest.fn();
const wrapper = shallow(
<Array
onChange={onChange}
knob={{ name: 'passions', value: ['Fishing', 'Skiing'], separator: ',' }}
/>
);
expect(deserialized).toEqual(['a', 'b', 'c']);
});
it('deserializes an Object to an Array', () => {
const object = { 1: 'one', 0: 'zero', 2: 'two' };
const deserialized = ArrayType.deserialize(object);
expect(deserialized).toEqual(['zero', 'one', 'two']);
wrapper.simulate('change', { target: { value: '' } });
expect(onChange).toHaveBeenCalledWith([]);
});
});

View File

@ -16,14 +16,29 @@ const styles = {
color: '#555',
};
const ArrayType = ({ knob, onChange }) => (
<Textarea
id={knob.name}
style={styles}
value={knob.value.join(knob.separator)}
onChange={e => onChange(e.target.value.split(knob.separator))}
/>
);
function formatArray(value, separator) {
if (value === '') {
return [];
}
return value.split(separator);
}
class ArrayType extends React.Component {
render() {
const { knob, onChange } = this.props;
return (
<Textarea
id={knob.name}
ref={c => {
this.input = c;
}}
style={styles}
value={knob.value.join(knob.separator)}
onChange={e => onChange(formatArray(e.target.value, knob.separator))}
/>
);
}
}
ArrayType.defaultProps = {
knob: {},

View File

@ -1,70 +1,31 @@
import { window } from 'global';
import deprecate from 'util-deprecate';
import addons from '@storybook/addons';
import KnobManager from './KnobManager';
import { vueHandler } from './vue';
import { reactHandler } from './react';
const manager = new KnobManager();
import {
array,
boolean,
button,
color,
date,
knob,
manager,
number,
object,
select,
text,
} from './base';
export function knob(name, options) {
return manager.knob(name, options);
}
export { knob, text, boolean, number, color, object, array, date, button, select };
export function text(name, value) {
return manager.knob(name, { type: 'text', value });
}
deprecate(() => {},
'Using @storybook/addon-knobs directly is discouraged, please use @storybook/addon-knobs/{{framework}}');
export function boolean(name, value) {
return manager.knob(name, { type: 'boolean', value });
}
export function number(name, value, options = {}) {
const defaults = {
range: false,
min: 0,
max: 10,
step: 1,
};
const mergedOptions = { ...defaults, ...options };
const finalOptions = {
...mergedOptions,
type: 'number',
value,
};
return manager.knob(name, finalOptions);
}
export function color(name, value) {
return manager.knob(name, { type: 'color', value });
}
export function object(name, value) {
return manager.knob(name, { type: 'object', value });
}
export function select(name, options, value) {
return manager.knob(name, { type: 'select', options, value });
}
export function array(name, value, separator = ',') {
return manager.knob(name, { type: 'array', value, separator });
}
export function date(name, value = new Date()) {
const proxyValue = value ? value.getTime() : null;
return manager.knob(name, { type: 'date', value: proxyValue });
}
export function button(name, callback) {
return manager.knob(name, { type: 'button', callback, hideLabel: true });
}
// "Higher order component" / wrapper style API
// In 3.3, this will become `withKnobs`, once our decorator API supports it.
// See https://github.com/storybooks/storybook/pull/1527
// generic higher-order component decorator for all platforms - usage is discouraged
// This file Should be removed with 4.0 release
function wrapperKnobs(options) {
const channel = addons.getChannel();
manager.setChannel(channel);

View File

@ -55,8 +55,8 @@ export default class WrapStory extends React.Component {
this.setState({ storyContent: storyFn(context) });
}
knobClicked(knob) {
const knobOptions = this.props.knobStore.get(knob.name);
knobClicked(clicked) {
const knobOptions = this.props.knobStore.get(clicked.name);
knobOptions.callback();
}

View File

@ -1,11 +1,43 @@
import React from 'react';
import addons from '@storybook/addons';
import WrapStory from './WrapStory';
/**
* Handles a react story
*/
import {
knob,
text,
boolean,
number,
color,
object,
array,
date,
select,
button,
manager,
} from '../base';
export { knob, text, boolean, number, color, object, array, date, select, button };
export const reactHandler = (channel, knobStore) => getStory => context => {
const initialContent = getStory(context);
const props = { context, storyFn: getStory, channel, knobStore, initialContent };
return <WrapStory {...props} />;
};
function wrapperKnobs(options) {
const channel = addons.getChannel();
manager.setChannel(channel);
if (options) channel.emit('addon:knobs:setOptions', options);
return reactHandler(channel, manager.knobStore);
}
export function withKnobs(storyFn, context) {
return wrapperKnobs()(storyFn)(context);
}
export function withKnobsOptions(options = {}) {
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
}

View File

@ -1,11 +0,0 @@
import * as tt from 'typescript-definition-tester';
describe('TypeScript definitions', () => {
it('should compile against index.d.ts', done => {
tt.compileDirectory(
`${__dirname}/../example/typescript`,
fileName => fileName.match(/\.ts$/),
() => done()
);
});
});

View File

@ -1,3 +1,21 @@
import addons from '@storybook/addons';
import {
knob,
text,
boolean,
number,
color,
object,
array,
date,
select,
button,
manager,
} from '../base';
export { knob, text, boolean, number, color, object, array, date, select, button };
export const vueHandler = (channel, knobStore) => getStory => context => ({
data() {
return {
@ -22,8 +40,8 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
this.$forceUpdate();
},
onKnobClick(knob) {
const knobOptions = knobStore.get(knob.name);
onKnobClick(clicked) {
const knobOptions = knobStore.get(clicked.name);
knobOptions.callback();
},
@ -53,3 +71,20 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
knobStore.unsubscribe(this.setPaneKnobs);
},
});
function wrapperKnobs(options) {
const channel = addons.getChannel();
manager.setChannel(channel);
if (options) channel.emit('addon:knobs:setOptions', options);
return vueHandler(channel, manager.knobStore);
}
export function withKnobs(storyFn, context) {
return wrapperKnobs()(storyFn)(context);
}
export function withKnobsOptions(options = {}) {
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
}

View File

@ -1,14 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"noImplicitAny": false,
"sourceMap": false,
"lib": [
"es2015",
"es2016",
"dom"
],
"jsx": "react"
}
}

1
addons/knobs/vue.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/vue');

View File

@ -19,6 +19,7 @@ This addon works with Storybook for:
## Getting Started
Install this addon by adding the `@storybook/addon-links` dependency:
```sh
yarn add @storybook/addon-links
```
@ -57,7 +58,74 @@ linkTo('Toggle') // Links to the first story in the 'Toggle' kind
With that, you can link an event in a component to any story in the Storybook.
- First parameter is the the story kind name (what you named with `storiesOf`).
-   Second (optional) parameter is the story name (what you named with `.add`). If the second parameter is omitted, the link will point to the first story in the given kind.
- Second (optional) parameter is the story name (what you named with `.add`).
If the second parameter is omitted, the link will point to the first story in the given kind.
> You can also pass a function instead for any of above parameter. That function accepts arguments emitted by the event and it should return a string. <br/>
> Have a look at [PR86](https://github.com/kadirahq/react-storybook/pull/86) for more information.
You can also pass a function instead for any of above parameter. That function accepts arguments emitted by the event and it should return a string:
```js
import { storiesOf } from '@storybook/react';
import { LinkTo, linkTo } from '@storybook/addon-links';
storiesOf('Select', module)
.add('Index', () => (
<select value="Index" onChange={linkTo('Select', e => e.currentTarget.value)}>
<option>Index</option>
<option>First</option>
<option>Second</option>
<option>Third</option>
</select>
))
.add('First', () => <LinkTo story="Index">Go back</LinkTo>)
.add('Second', () => <LinkTo story="Index">Go back</LinkTo>)
.add('Third', () => <LinkTo story="Index">Go back</LinkTo>);
```
## hrefTo function
If you want to get an URL for a particular story, you may use `hrefTo` function. It returns a promise, which resolves to string containing a relative URL:
```js
import { storiesOf } from '@storybook/react';
import { hrefTo } from '@storybook/addon-links';
import { action } from '@storybook/addon-actions';
storiesOf('Href', module)
.add('log', () => {
hrefTo('Href', 'log').then(action('URL of this story'));
return <span>See action logger</span>;
});
```
## LinkTo component (React only)
One possible way of using `hrefTo` is to create a component that uses native `a` element, but prevents page reloads on plain left click, so that one can still use default browser methods to open link in new tab.
A React implementation of such a component can be imported from `@storybook/addon-links` package:
```js
import { storiesOf } from '@storybook/react';
import { LinkTo } from '@storybook/addon-links';
storiesOf('Link', module)
.add('First', () => (
<LinkTo story="Second">Go to Second</LinkTo>
))
.add('Second', () => (
<LinkTo story="First">Go to First</LinkTo>
));
```
It accepts all the props the `a` element does, plus `story` and `kind`. It the `kind` prop is omitted, the current kind will be preserved.
```js
<LinkTo
kind="Toggle"
story="off"
target="_blank"
title="link to second story"
style={{color: '#1474f3'}}
>Go to Second</LinkTo>
```
To implement such a component for another framework, you need to add special handling for `click` event on native `a` element. See [`RoutedLink` sources](https://github.com/storybooks/storybook/blob/master/lib/components/src/navigation/routed_link.js#L4-L9) for reference.

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-links",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Story Links addon for storybook",
"keywords": [
"storybook"
@ -21,7 +21,16 @@
"storybook": "start-storybook -p 9001"
},
"dependencies": {
"@storybook/addons": "^3.2.16"
"@storybook/addons": "^3.3.0-alpha.2",
"@storybook/components": "^3.3.0-alpha.2",
"global": "^4.3.2",
"prop-types": "^15.5.10"
},
"devDependencies": {
"enzyme": "^3.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"shelljs": "^0.7.8"
},
"peerDependencies": {
"react": "*",

View File

@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LinkTo render should render a link 1`] = `
<RoutedLink
href="?selectedKind=undefined&selectedStory=undefined"
onClick={[Function]}
/>
`;

View File

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { RoutedLink } from '@storybook/components';
import { openLink, hrefTo } from '../preview';
export default class LinkTo extends PureComponent {
constructor(...args) {
super(...args);
this.state = {
href: '/',
};
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
this.updateHref(this.props);
}
componentWillReceiveProps(props) {
if (props.kind !== this.props.kind || props.story !== this.props.story) {
this.updateHref(props);
}
}
async updateHref(props) {
const href = await hrefTo(props.kind, props.story);
this.setState({ href });
}
handleClick() {
openLink(this.props);
}
render() {
const { kind, story, ...rest } = this.props;
return <RoutedLink href={this.state.href} onClick={this.handleClick} {...rest} />;
}
}
LinkTo.defaultProps = {
kind: null,
story: null,
};
LinkTo.propTypes = {
kind: PropTypes.string,
story: PropTypes.string,
};

View File

@ -0,0 +1,40 @@
import { shallow } from 'enzyme';
import React from 'react';
import addons from '@storybook/addons';
import { EVENT_ID } from '..';
import { mockChannel } from '../preview.test';
import LinkTo from './link';
jest.mock('@storybook/addons');
describe('LinkTo', () => {
describe('render', () => {
it('should render a link', async () => {
const channel = mockChannel();
addons.getChannel.mockReturnValue(channel);
const wrapper = shallow(<LinkTo kind="foo" story="bar" />);
await wrapper.instance().updateHref(wrapper.props());
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
describe('events', () => {
it('should select the kind and story on click', () => {
const channel = {
emit: jest.fn(),
on: jest.fn(),
};
addons.getChannel.mockReturnValue(channel);
const wrapper = shallow(<LinkTo kind="foo" story="bar" />);
wrapper.simulate('click');
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
kind: 'foo',
story: 'bar',
});
});
});
});

View File

@ -1,5 +1,8 @@
export const ADDON_ID = 'storybook/links';
export const EVENT_ID = `${ADDON_ID}/link-event`;
export const REQUEST_HREF_EVENT_ID = `${ADDON_ID}/request-href-event`;
export const RECEIVE_HREF_EVENT_ID = `${ADDON_ID}/receive-href-event`;
export { register } from './manager';
export { linkTo } from './preview';
export { linkTo, hrefTo } from './preview';
export { default as LinkTo } from './components/link';

View File

@ -1,11 +1,29 @@
import { location } from 'global';
import addons from '@storybook/addons';
import { ADDON_ID, EVENT_ID } from './';
import { ADDON_ID, EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
export function register() {
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
channel.on(EVENT_ID, selection => {
api.selectStory(selection.kind, selection.story);
if (selection.kind != null) {
api.selectStory(selection.kind, selection.story);
} else {
api.selectInCurrentKind(selection.story);
}
});
channel.on(REQUEST_HREF_EVENT_ID, selection => {
const params =
selection.kind != null
? {
selectedKind: selection.kind,
selectedStory: selection.story,
}
: {
selectedStory: selection.story,
};
const urlState = api.getUrlState(params);
channel.emit(RECEIVE_HREF_EVENT_ID, location.pathname + urlState.url);
});
});
}

View File

@ -1,12 +1,21 @@
import addons from '@storybook/addons';
import { EVENT_ID } from './';
import { EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
export function linkTo(kind, story) {
return (...args) => {
const resolvedKind = typeof kind === 'function' ? kind(...args) : kind;
const resolvedStory = typeof story === 'function' ? story(...args) : story;
export const openLink = params => addons.getChannel().emit(EVENT_ID, params);
const valueOrCall = args => value => (typeof value === 'function' ? value(...args) : value);
export const linkTo = (kind, story) => (...args) => {
const resolver = valueOrCall(args);
openLink({
kind: resolver(kind),
story: resolver(story),
});
};
export const hrefTo = (kind, story) =>
new Promise(resolve => {
const channel = addons.getChannel();
channel.emit(EVENT_ID, { kind: resolvedKind, story: resolvedStory });
};
}
channel.on(RECEIVE_HREF_EVENT_ID, resolve);
channel.emit(REQUEST_HREF_EVENT_ID, { kind, story });
});

View File

@ -0,0 +1,61 @@
import addons from '@storybook/addons';
import { linkTo, hrefTo } from './preview';
import { EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
jest.mock('@storybook/addons');
export const mockChannel = () => {
let cb;
return {
emit(id, payload) {
if (id === REQUEST_HREF_EVENT_ID) {
cb(`?selectedKind=${payload.kind}&selectedStory=${payload.story}`);
}
},
on(id, callback) {
if (id === RECEIVE_HREF_EVENT_ID) {
cb = callback;
}
},
};
};
describe('preview', () => {
describe('linkTo()', () => {
it('should select the kind and story provided', () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);
const handler = linkTo('kind', 'story');
handler();
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
kind: 'kind',
story: 'story',
});
});
it('should handle functions returning strings', () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);
const handler = linkTo((a, b) => a + b, (a, b) => b + a);
handler('foo', 'bar');
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
kind: 'foobar',
story: 'barfoo',
});
});
});
describe('hrefTo()', () => {
it('should return promise resolved with story href', async () => {
const channel = mockChannel();
addons.getChannel.mockReturnValue(channel);
const href = await hrefTo('kind', 'story');
expect(href).toBe('?selectedKind=kind&selectedStory=story');
});
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-notes",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Write notes for your Storybook stories.",
"keywords": [
"addon",
@ -19,7 +19,7 @@
"storybook": "start-storybook -p 9010"
},
"dependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/addons": "^3.3.0-alpha.2",
"babel-runtime": "^6.26.0",
"prop-types": "^15.6.0",
"util-deprecate": "^1.0.2"

View File

@ -5,10 +5,10 @@ setOptions({
name: 'CUSTOM-OPTIONS',
url: 'https://github.com/storybooks/storybook',
// goFullScreen: false,
// showLeftPanel: true,
showDownPanel: false,
// showStoriesPanel: true,
showAddonPanel: false,
// showSearchBox: false,
// downPanelInRight: false,
// addonPanelInRight: false,
});
storybook.configure(() => require('./stories'), module);

View File

@ -56,25 +56,25 @@ setOptions({
*/
goFullScreen: false,
/**
* display left panel that shows a list of stories
* display panel that shows a list of stories
* @type {Boolean}
*/
showLeftPanel: true,
showStoriesPanel: true,
/**
* display horizontal panel that displays addon configurations
* display panel that shows addon configurations
* @type {Boolean}
*/
showDownPanel: true,
showAddonPanel: true,
/**
* display floating search box to search through stories
* @type {Boolean}
*/
showSearchBox: false,
/**
* show horizontal addons panel as a vertical panel on the right
* show addon panel as a vertical panel on the right
* @type {Boolean}
*/
downPanelInRight: false,
addonPanelInRight: false,
/**
* sorts stories
* @type {Boolean}
@ -101,7 +101,7 @@ setOptions({
* id to select an addon panel
* @type {String}
*/
selectedAddonPanel: undefined, // The order of addons in the "Addons Panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook
selectedAddonPanel: undefined, // The order of addons in the "Addon panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook
});
storybook.configure(() => require('./stories'), module);

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-options",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "Options addon for storybook",
"keywords": [
"storybook"
@ -20,7 +20,13 @@
"storybook": "start-storybook -p 9001"
},
"dependencies": {
"@storybook/addons": "^3.2.16"
"@storybook/addons": "^3.3.0-alpha.2"
},
"devDependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-test-renderer": "^16.0.0",
"shelljs": "^0.7.8"
},
"peerDependencies": {
"react": "*",

View File

@ -147,6 +147,36 @@ Just render the story, don't check the output at all (useful if you just want to
Like the default, but allows you to specify a set of options for the test renderer. [See for example here](https://github.com/storybooks/storybook/blob/b915b5439786e0edb17d7f5ab404bba9f7919381/examples/test-cra/src/storyshots.test.js#L14-L16).
### `multiSnapshotWithOptions(options)`
Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes.
### `shallowSnapshot`
Take a snapshot of a shallow-rendered version of the component.
### `getSnapshotFileName`
Utility function used in `multiSnapshotWithOptions`. This is made available for users who implement custom test functions that also want to take advantage of multi-file storyshots.
###### Example:
Let's say we wanted to create a test function for shallow && multi-file snapshots:
```js
import initStoryshots, { getSnapshotFileName } from '@storybook/addon-storyshots';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
initStoryshots({
test: ({ story, context }) => {
const snapshotFileName = getSnapshotFileName(context);
const storyElement = story.render(context);
const shallowTree = shallow(storyElement);
if (snapshotFileName) {
expect(toJson(shallowTree)).toMatchSpecificSnapshot(snapshotFileName);
}
}
});
```

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-storyshots",
"version": "3.2.16",
"version": "3.3.0-alpha.2",
"description": "StoryShots is a Jest Snapshot Testing Addon for Storybook.",
"license": "MIT",
"main": "dist/index.js",
@ -11,23 +11,35 @@
"scripts": {
"build-storybook": "build-storybook",
"prepare": "babel ./src --out-dir ./dist",
"storybook": "start-storybook -p 6006"
"storybook": "start-storybook -p 6006",
"example": "jest storyshot.test"
},
"dependencies": {
"babel-runtime": "^6.26.0",
"glob": "^7.1.2",
"global": "^4.3.2",
"jest-specific-snapshot": "^0.2.0",
"prop-types": "^15.6.0",
"read-pkg-up": "^3.0.0"
},
"devDependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/channels": "^3.2.16",
"@storybook/react": "^3.2.16"
"@storybook/addons": "^3.3.0-alpha.2",
"@storybook/channels": "^3.3.0-alpha.2",
"@storybook/react": "^3.3.0-alpha.2",
"babel-cli": "^6.26.0",
"babel-jest": "^20.0.3",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"jest": "^20.0.4",
"jest-cli": "^20.0.4",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"peerDependencies": {
"@storybook/addons": "^3.2.16",
"@storybook/channels": "^3.2.16",
"@storybook/react": "^3.2.16",
"@storybook/addons": "^3.3.0-alpha.2",
"@storybook/channels": "^3.3.0-alpha.2",
"@storybook/react": "^3.3.0-alpha.2",
"babel-core": "^6.26.0",
"react": "*",
"react-test-renderer": "*"

View File

@ -1,5 +1,6 @@
import path from 'path';
import fs from 'fs';
import glob from 'glob';
import global, { describe, it } from 'global';
import readPkgUp from 'read-pkg-up';
import addons from '@storybook/addons';
@ -7,8 +8,17 @@ import addons from '@storybook/addons';
import runWithRequireContext from './require_context';
import createChannel from './storybook-channel-mock';
import { snapshot } from './test-bodies';
import { getPossibleStoriesFiles, getSnapshotFileName } from './utils';
export { snapshotWithOptions, snapshot, shallowSnapshot, renderOnly } from './test-bodies';
export {
snapshot,
multiSnapshotWithOptions,
snapshotWithOptions,
shallowSnapshot,
renderOnly,
} from './test-bodies';
export { getSnapshotFileName };
let storybook;
let configPath;
@ -50,6 +60,7 @@ export default function testStorySnapshots(options = {}) {
runWithRequireContext(content, contextOpts);
} else if (isRNStorybook) {
storybook = require.requireActual('@storybook/react-native');
configPath = path.resolve(options.configPath || 'storybook');
require.requireActual(configPath);
} else {
@ -64,6 +75,10 @@ export default function testStorySnapshots(options = {}) {
const suite = options.suite || options.suit || 'Storyshots';
const stories = storybook.getStorybook();
if (stories.length === 0) {
throw new Error('storyshots found 0 stories');
}
// Added not to break existing storyshots configs (can be removed in a future major release)
// eslint-disable-next-line
options.storyNameRegex = options.storyNameRegex || options.storyRegex;
@ -72,13 +87,15 @@ export default function testStorySnapshots(options = {}) {
// eslint-disable-next-line
for (const group of stories) {
if (options.storyKindRegex && !group.kind.match(options.storyKindRegex)) {
const { fileName, kind } = group;
if (options.storyKindRegex && !kind.match(options.storyKindRegex)) {
// eslint-disable-next-line
continue;
}
describe(suite, () => {
describe(group.kind, () => {
describe(kind, () => {
// eslint-disable-next-line
for (const story of group.stories) {
if (options.storyNameRegex && !story.name.match(options.storyNameRegex)) {
@ -87,11 +104,24 @@ export default function testStorySnapshots(options = {}) {
}
it(story.name, () => {
const context = { kind: group.kind, story: story.name };
return options.test({ story, context });
const context = { fileName, kind, story: story.name };
options.test({ story, context });
});
}
});
});
}
}
describe('Storyshots Integrity', () => {
describe('Abandoned Storyshots', () => {
const storyshots = glob.sync('**/*.storyshot');
const abandonedStoryshots = storyshots.filter(fileName => {
const possibleStoriesFiles = getPossibleStoriesFiles(fileName);
return !possibleStoriesFiles.some(fs.existsSync);
});
expect(abandonedStoryshots).toHaveLength(0);
});
});

View File

@ -1,12 +1,30 @@
import renderer from 'react-test-renderer';
import shallow from 'react-test-renderer/shallow';
import 'jest-specific-snapshot';
import { getSnapshotFileName } from './utils';
function getRenderedTree(story, context, options) {
const storyElement = story.render(context);
return renderer.create(storyElement, options).toJSON();
}
export const snapshotWithOptions = options => ({ story, context }) => {
const storyElement = story.render(context);
const tree = renderer.create(storyElement, options).toJSON();
const tree = getRenderedTree(story, context, options);
expect(tree).toMatchSnapshot();
};
export const multiSnapshotWithOptions = options => ({ story, context }) => {
const tree = getRenderedTree(story, context, options);
const snapshotFileName = getSnapshotFileName(context);
if (!snapshotFileName) {
expect(tree).toMatchSnapshot();
return;
}
expect(tree).toMatchSpecificSnapshot(snapshotFileName);
};
export const snapshot = snapshotWithOptions({});
export function shallowSnapshot({ story, context }) {

View File

@ -0,0 +1,25 @@
import path from 'path';
function getStoryshotFile(fileName) {
const { dir, name } = path.parse(fileName);
return path.format({ dir: path.join(dir, '__snapshots__'), name, ext: '.storyshot' });
}
export function getPossibleStoriesFiles(storyshotFile) {
const { dir, name } = path.parse(storyshotFile);
return [
path.format({ dir: path.dirname(dir), name, ext: '.js' }),
path.format({ dir: path.dirname(dir), name, ext: '.jsx' }),
];
}
export function getSnapshotFileName(context) {
const { fileName } = context;
if (!fileName) {
return null;
}
return getStoryshotFile(fileName);
}

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Another Button with some emoji 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
😀 😎 👍 💯
</button>
`;
exports[`Storyshots Another Button with text 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
Hello Button
</button>
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Button with some emoji 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
😀 😎 👍 💯
</button>
`;
exports[`Storyshots Button with text 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
Hello Button
</button>
`;

View File

@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Welcome to Storybook 1`] = `
<article
className="css-1fqbdip"
>
<h1
className="css-nil"
>
Welcome to storybook
</h1>
<p>
This is a UI component dev environment for your app.
</p>
<p>
We've added some basic stories inside the
<code
className="css-mteq83"
>
src/stories
</code>
directory.
<br />
A story is a single state of one or more UI components. You can have as many stories as you want.
<br />
(Basically a story is like a visual test case.)
</p>
<p>
See these sample
<a
className="css-ca0824"
onClick={[Function]}
role="button"
tabIndex="0"
>
stories
</a>
for a component called
<code
className="css-mteq83"
>
Button
</code>
.
</p>
<p>
Just like that, you can add your own components as stories.
<br />
You can also edit those components and see changes right away.
<br />
(Try editing the
<code
className="css-mteq83"
>
Button
</code>
stories located at
<code
className="css-mteq83"
>
src/stories/index.js
</code>
.)
</p>
<p>
Usually we create stories with smaller UI components in the app.
<br />
Have a look at the
<a
className="css-ca0824"
href="https://storybook.js.org/basics/writing-stories"
rel="noopener noreferrer"
target="_blank"
>
Writing Stories
</a>
section in our documentation.
</p>
<p
className="css-bwdon3"
>
<b>
NOTE:
</b>
<br />
Have a look at the
<code
className="css-mteq83"
>
.storybook/webpack.config.js
</code>
to add webpack loaders and plugins you are using in this project.
</p>
</article>
`;

View File

@ -0,0 +1,8 @@
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions } from '../src';
initStoryshots({
framework: 'react',
configPath: path.join(__dirname, '..', '.storybook'),
test: multiSnapshotWithOptions({}),
});

37
addons/viewport/README.md Normal file
View File

@ -0,0 +1,37 @@
# Storybook Viewport Addon
Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in [Storybook](https://storybookjs.org). This helps build responsive components inside of Storybook.
This addon works with Storybook for: [React](https://github.com/storybooks/storybook/tree/master/app/react) and [Vue](https://github.com/storybooks/storybook/tree/master/app/vue).
![Screenshot](https://github.com/storybooks/storybook/blob/master/docs/viewport.png)
## Installation
Install the following npm module:
npm i --save-dev @storybook/addon-viewport
or with yarn:
yarn add -D @storybook/addon-viewport
## Basic Usage
Simply import the Storybook Viewport Addon in the `addon.js` file in your `.storybook` directory.
```js
import '@storybook/addon-viewport/register'
```
This will register the Viewport Addon to Storybook and will show up in the action area.
## FAQ
#### How do I add a new device?
Unfortunately, this is currently not supported.
#### How can I make a custom screen size?
Unfortunately, this is currently not supported.

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@ -0,0 +1,22 @@
{
"name": "@storybook/addon-viewport",
"version": "3.3.0-alpha.2",
"description": "Storybook addon to change the viewport size to mobile",
"main": "dist/index.js",
"keywords": [
"storybook"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"license": "MIT",
"dependencies": {
"@storybook/components": "^3.3.0-alpha.2",
"global": "^4.3.2",
"prop-types": "^15.5.10"
},
"peerDependencies": {
"@storybook/addons": "^3.2.0",
"react": "*"
}
}

View File

@ -0,0 +1,119 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { baseFonts } from '@storybook/components';
import { document } from 'global';
import { viewports, defaultViewport, resetViewport } from './viewportInfo';
import { SelectViewport } from './SelectViewport';
import { RotateViewport } from './RotateViewport';
import * as styles from './styles';
const storybookIframe = 'storybook-preview-iframe';
const containerStyles = {
padding: 15,
width: '100%',
boxSizing: 'border-box',
...baseFonts,
};
export class Panel extends Component {
static propTypes = {
channel: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
constructor(props, context) {
super(props, context);
this.state = {
viewport: defaultViewport,
isLandscape: false,
};
this.props.channel.on('addon:viewport:update', this.changeViewport);
}
componentDidMount() {
this.iframe = document.getElementById(storybookIframe);
}
componentWillUnmount() {
this.props.channel.removeListener('addon:viewport:update', this.changeViewport);
}
iframe = undefined;
changeViewport = viewport => {
const { viewport: previousViewport } = this.state;
if (previousViewport !== viewport) {
this.setState(
{
viewport,
isLandscape: false,
},
this.updateIframe
);
}
};
toggleLandscape = () => {
const { isLandscape } = this.state;
this.setState({ isLandscape: !isLandscape }, this.updateIframe);
};
updateIframe = () => {
const { viewport: viewportKey, isLandscape } = this.state;
const viewport = viewports[viewportKey] || resetViewport;
if (!this.iframe) {
throw new Error('Cannot find Storybook iframe');
}
Object.keys(viewport.styles).forEach(prop => {
this.iframe.style[prop] = viewport.styles[prop];
});
if (isLandscape) {
this.iframe.style.height = viewport.styles.width;
this.iframe.style.width = viewport.styles.height;
}
};
render() {
const { isLandscape, viewport } = this.state;
const disableDefault = viewport === defaultViewport;
const disabledStyles = disableDefault ? styles.disabled : {};
const buttonStyles = {
...styles.button,
...disabledStyles,
marginTop: 30,
padding: 20,
};
return (
<div style={containerStyles}>
<SelectViewport
activeViewport={viewport}
onChange={e => this.changeViewport(e.target.value)}
/>
<RotateViewport
onClick={this.toggleLandscape}
disabled={disableDefault}
active={isLandscape}
/>
<button
style={buttonStyles}
onClick={() => this.changeViewport(defaultViewport)}
disabled={disableDefault}
>
Reset Viewport
</button>
</div>
);
}
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as styles from './styles';
export function RotateViewport({ active, ...props }) {
const disabledStyles = props.disabled ? styles.disabled : {};
const actionStyles = {
...styles.action,
...disabledStyles,
};
return (
<div style={styles.row}>
<label style={styles.label}>Rotate</label>
<button {...props} style={actionStyles}>
{active ? 'Vertical' : 'Landscape'}
</button>
</div>
);
}
RotateViewport.propTypes = {
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired,
active: PropTypes.bool,
};
RotateViewport.defaultProps = {
disabled: true,
active: false,
};

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { viewports, defaultViewport } from './viewportInfo';
import * as styles from './styles';
export function SelectViewport({ activeViewport, onChange }) {
return (
<div style={styles.row}>
<label style={styles.label}>Device</label>
<select style={styles.action} value={activeViewport} onChange={onChange}>
<option value={defaultViewport}>Default</option>
{Object.keys(viewports).map(key => (
<option value={key} key={key}>
{viewports[key].name}
</option>
))}
</select>
</div>
);
}
SelectViewport.propTypes = {
onChange: PropTypes.func.isRequired,
activeViewport: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,30 @@
export const row = {
width: '100%',
display: 'flex',
marginBottom: 15,
};
export const label = {
width: 80,
marginRight: 15,
};
const actionColor = 'rgb(247, 247, 247)';
export const button = {
color: 'rgb(85, 85, 85)',
width: '100%',
border: `1px solid ${actionColor}`,
backgroundColor: actionColor,
borderRadius: 3,
};
export const disabled = {
opacity: '0.5',
cursor: 'not-allowed',
};
export const action = {
...button,
height: 30,
};

View File

@ -0,0 +1,249 @@
import React from 'react';
import { shallow } from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies
import { document } from 'global';
import { Panel } from '../Panel';
import { viewports, defaultViewport, resetViewport } from '../viewportInfo';
import * as styles from '../styles';
describe('Viewport/Panel', () => {
const props = {
channel: {
on: jest.fn(),
removeListener: jest.fn(),
},
};
let subject;
beforeEach(() => {
subject = shallow(<Panel {...props} />);
});
describe('construct', () => {
it('creates the initial state', () => {
expect(subject.instance().state).toEqual({
viewport: defaultViewport,
isLandscape: false,
});
});
it('listens on the channel', () => {
expect(props.channel.on).toHaveBeenCalledWith(
'addon:viewport:update',
subject.instance().changeViewport
);
});
});
describe('componentDidMount', () => {
let previousGet;
beforeEach(() => {
subject.instance().iframe = undefined;
previousGet = document.getElementById;
document.getElementById = jest.fn(() => 'iframe');
subject.instance().componentDidMount();
});
afterEach(() => {
document.getElementById = previousGet;
});
it('gets the iframe', () => {
expect(subject.instance().iframe).toEqual('iframe');
});
});
describe('componentWillUnmount', () => {
beforeEach(() => {
subject.instance().componentWillUnmount();
});
it('removes the channel listener', () => {
expect(props.channel.removeListener).toHaveBeenCalledWith(
'addon:viewport:update',
subject.instance().changeViewport
);
});
});
describe('changeViewport', () => {
beforeEach(() => {
subject.instance().setState = jest.fn();
subject.instance().updateIframe = jest.fn();
});
describe('new viewport', () => {
beforeEach(() => {
subject.instance().changeViewport(viewports[0]);
});
it('sets the state with the new information', () => {
expect(subject.instance().setState).toHaveBeenCalledWith(
{
viewport: viewports[0],
isLandscape: false,
},
subject.instance().updateIframe
);
});
});
describe('same as previous viewport', () => {
beforeEach(() => {
subject.instance().changeViewport(defaultViewport);
});
it('doesnt update the state', () => {
expect(subject.instance().setState).not.toHaveBeenCalled();
});
});
});
describe('toggleLandscape', () => {
beforeEach(() => {
subject.setState({ isLandscape: false });
subject.instance().setState = jest.fn();
subject.instance().toggleLandscape();
});
it('updates the landscape to be the opposite', () => {
expect(subject.instance().setState).toHaveBeenCalledWith(
{ isLandscape: true },
subject.instance().updateIframe
);
});
});
describe('updateIframe', () => {
let iframe;
describe('no iframe found', () => {
beforeEach(() => {
subject.instance().iframe = null;
});
it('throws a TypeError', () => {
expect(() => {
subject.instance().updateIframe();
}).toThrow('Cannot find Storybook iframe');
});
});
describe('iframe found', () => {
beforeEach(() => {
iframe = { style: {} };
subject.instance().iframe = iframe;
});
it('sets the viewport information on the iframe', () => {
subject.instance().updateIframe();
expect(subject.instance().iframe.style).toEqual(resetViewport.styles);
});
it('swaps the height/width when in landscape', () => {
subject.instance().state.isLandscape = true;
subject.instance().updateIframe();
expect(subject.instance().iframe.style).toEqual(
expect.objectContaining({
height: resetViewport.styles.width,
width: resetViewport.styles.height,
})
);
});
});
});
describe('render', () => {
describe('reset button', () => {
let resetBtn;
beforeEach(() => {
subject.instance().changeViewport = jest.fn();
resetBtn = subject.find('button');
});
it('styles the reset button as disabled if viewport is default', () => {
expect(resetBtn.props().style).toEqual(expect.objectContaining(styles.disabled));
});
it('enabels the reset button if not default', () => {
subject.setState({ viewport: 'iphone6' });
// Find updated button
resetBtn = subject.find('button');
expect(resetBtn.props().style).toEqual({
...styles.button,
marginTop: 30,
padding: 20,
});
});
it('toggles the landscape on click', () => {
resetBtn.simulate('click');
expect(subject.instance().changeViewport).toHaveBeenCalledWith(defaultViewport);
});
});
describe('SelectViewport', () => {
let select;
beforeEach(() => {
select = subject.find('SelectViewport');
subject.instance().changeViewport = jest.fn();
});
it('passes the activeViewport', () => {
expect(select.props()).toEqual(
expect.objectContaining({
activeViewport: defaultViewport,
})
);
});
it('onChange it updates the viewport', () => {
const e = { target: { value: 'iphone6' } };
select.simulate('change', e);
expect(subject.instance().changeViewport).toHaveBeenCalledWith(e.target.value);
});
});
describe('RotateView', () => {
let toggle;
beforeEach(() => {
toggle = subject.find('RotateViewport');
jest.spyOn(subject.instance(), 'toggleLandscape');
subject.instance().forceUpdate();
});
it('passes the active prop based on the state of the panel', () => {
expect(toggle.props().active).toEqual(subject.state('isLandscape'));
});
describe('is on the default viewport', () => {
beforeEach(() => {
subject.setState({ viewport: defaultViewport });
});
it('sets the disabled property', () => {
expect(toggle.props().disabled).toEqual(true);
});
});
describe('is on a responsive viewport', () => {
beforeEach(() => {
subject.setState({ viewport: 'iphone6' });
toggle = subject.find('RotateViewport');
});
it('the disabled property is false', () => {
expect(toggle.props().disabled).toEqual(false);
});
});
});
});
});

View File

@ -0,0 +1,91 @@
import React from 'react';
import { shallow } from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies
import { RotateViewport } from '../RotateViewport';
import * as styles from '../styles';
describe('Viewport/RotateViewport', () => {
let subject;
let props;
beforeEach(() => {
props = {
onClick: jest.fn(),
};
subject = shallow(<RotateViewport {...props} />);
});
it('has a label', () => {
expect(subject.find('label').text()).toEqual('Rotate');
});
describe('button', () => {
let btn;
beforeEach(() => {
btn = subject.find('button');
});
it('has a click handler set via props', () => {
// note, this shouldn't trigger if disabled, but enzyme doesn't care
btn.simulate('click');
expect(props.onClick).toHaveBeenCalled();
});
it('renders the the action styles on the button', () => {
expect(btn.props().style).toEqual(expect.objectContaining(styles.action));
});
describe('is active', () => {
beforeEach(() => {
subject.setProps({ active: true });
btn = subject.find('button');
});
it('renders the correct text', () => {
expect(btn.text()).toEqual('Vertical');
});
});
describe('is inactive', () => {
beforeEach(() => {
subject.setProps({ active: false });
btn = subject.find('button');
});
it('renders the correct text', () => {
expect(btn.text()).toEqual('Landscape');
});
});
describe('is disabled', () => {
beforeEach(() => {
subject.setProps({ disabled: true });
btn = subject.find('button');
});
it('renders the disabled styles', () => {
expect(btn.props().style).toEqual(expect.objectContaining(styles.disabled));
});
it('sets the disabled property on the button', () => {
expect(btn.props().disabled).toEqual(true);
});
});
describe('is enabled', () => {
beforeEach(() => {
subject.setProps({ disabled: false });
btn = subject.find('button');
});
it('renders the disabled styles', () => {
expect(btn.props().style).not.toEqual(expect.objectContaining(styles.disabled));
});
it('does not set the disabled property on the button', () => {
expect(btn.props().disabled).toEqual(false);
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More