diff --git a/.babelrc.js b/.babelrc.js index 8559a13..d4f92fb 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -4,6 +4,7 @@ module.exports = { "@babel/preset-typescript", "@babel/preset-react", ], + ignore: ["./src/typings.d.ts"], env: { esm: { presets: [ diff --git a/.gitignore b/.gitignore index a951d51..813bba0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ storybook-static/ build-storybook.log .DS_Store -.env \ No newline at end of file +.env +yarn-*.log \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index 4f864dc..9b02a5a 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -3,5 +3,15 @@ module.exports = { "../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)", ], - addons: ["../preset.js", "@storybook/addon-essentials"], + addons: [ + { + name: "../preset.js", + options: { useExternalInstrumentation: false } + }, + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + features: { + interactionsDebugger: true + } }; diff --git a/README.md b/README.md index 6b3bfb2..e201951 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,8 @@ -# Storybook Addon Addon coverage -Tools to support code coverage in Storybook +# Storybook Addon Coverage +Tools to support code coverage in Storybook and the [test runner](https://github.com/storybookjs/test-runner) + ### Development scripts - `yarn start` runs babel in watch mode and starts Storybook - `yarn build` build and package your addon code - -### Switch from TypeScript to JavaScript - -Don't want to use TypeScript? We offer a handy eject command: `yarn eject-ts` - -This will convert all code to JS. It is a destructive process, so we recommended running this before you start writing any code. - -## What's included? - -![Demo](https://user-images.githubusercontent.com/42671/107857205-e7044380-6dfa-11eb-8718-ad02e3ba1a3f.gif) - -The addon code lives in `src`. It demonstrates all core addon related concepts. The three [UI paradigms](https://storybook.js.org/docs/react/addons/addon-types#ui-based-addons) - -- `src/Tool.js` -- `src/Panel.js` -- `src/Tab.js` - -Which, along with the addon itself, are registered in `src/preset/manager.js`. - -Managing State and interacting with a story: - -- `src/withGlobals.js` & `src/Tool.js` demonstrates how to use `useGlobals` to manage global state and modify the contents of a Story. -- `src/withRoundTrip.js` & `src/Panel.js` demonstrates two-way communication using channels. -- `src/Tab.js` demonstrates how to use `useParameter` to access the current story's parameters. - -Your addon might use one or more of these patterns. Feel free to delete unused code. Update `src/preset/manager.js` and `src/preset/preview.js` accordingly. - -Lastly, configure you addon name in `src/constants.js`. - -### Metadata - -Storybook addons are listed in the [catalog](https://storybook.js.org/addons) and distributed via npm. The catalog is populated by querying npm's registry for Storybook-specific metadata in `package.json`. This project has been configured with sample data. Learn more about available options in the [Addon metadata docs](https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata). - -## Release Management - -### Setup - -This project is configured to use [auto](https://github.com/intuit/auto) for release management. It generates a changelog and pushes it to both GitHub and npm. Therefore, you need to configure access to both: - -- [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-access-tokens) Create a token with both _Read and Publish_ permissions. -- [`GH_TOKEN`](https://github.com/settings/tokens) Create a token with the `repo` scope. - -Then open your `package.json` and edit the following fields: - -- `name` -- `author` -- `repository` - -#### Local - -To use `auto` locally create a `.env` file at the root of your project and add your tokens to it: - -```bash -GH_TOKEN= -NPM_TOKEN= -``` - -Lastly, **create labels on GitHub**. You’ll use these labels in the future when making changes to the package. - -```bash -npx auto create-labels -``` - -If you check on GitHub, you’ll now see a set of labels that `auto` would like you to use. Use these to tag future pull requests. - -#### GitHub Actions - -This template comes with GitHub actions already set up to publish your addon anytime someone pushes to your repository. - -Go to `Settings > Secrets`, click `New repository secret`, and add your `NPM_TOKEN`. - -### Creating a release - -To create a release locally you can run the following command, otherwise the GitHub action will make the release for you. - -```sh -yarn release -``` - -That will: - -- Build and package the addon code -- Bump the version -- Push a release to GitHub and npm -- Push a changelog to GitHub diff --git a/babel-plugin-source.js b/babel-plugin-source.js new file mode 100644 index 0000000..39dd839 --- /dev/null +++ b/babel-plugin-source.js @@ -0,0 +1,20 @@ +module.exports = function ({ types: t }) { + return { + visitor: { + ExportDefaultDeclaration: { + enter({ node }) { + // set default.parameters.coverage + /** + * { + * coverage: { + * fileName: '', + * filePath: '', + * source: '' <- raw source + * } + * } + */ + }, + }, + }, + }; +}; \ No newline at end of file diff --git a/package.json b/package.json index 9456725..9a71db9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,12 @@ "version": "0.0.0", "description": "Tools to support code coverage in Storybook", "keywords": [ - "storybook-addons", "coverage", "test", "testing", "test-runner", "storybook-addons" + "storybook-addons", + "coverage", + "test", + "testing", + "test-runner", + "storybook-addons" ], "repository": { "type": "git", @@ -43,8 +48,10 @@ "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.5", "@babel/preset-typescript": "^7.13.0", - "@storybook/addon-essentials": "^6.4.0", - "@storybook/react": "^6.4.0", + "@storybook/addon-essentials": "^6.5.9", + "@storybook/addon-interactions": "^6.5.9", + "@storybook/react": "^6.5.9", + "@storybook/testing-library": "^0.0.12", "auto": "^10.3.0", "babel-loader": "^8.1.0", "boxen": "^5.0.1", @@ -52,6 +59,7 @@ "dedent": "^0.7.0", "prettier": "^2.3.1", "prop-types": "^15.7.2", + "raw-loader": "^4.0.2", "react": "^17.0.1", "react-dom": "^17.0.1", "rimraf": "^3.0.2", @@ -59,11 +67,10 @@ "zx": "^1.14.1" }, "peerDependencies": { - "@storybook/addons": "^6.4.0", - "@storybook/api": "^6.4.0", - "@storybook/components": "^6.4.0", - "@storybook/core-events": "^6.4.0", - "@storybook/theming": "^6.4.0", + "@storybook/addons": "^6", + "@storybook/api": "^6", + "@storybook/components": "^6", + "@storybook/core-events": "^6", "react": "^16.8.0 || ^17.0.0", "react-dom": "^16.8.0 || ^17.0.0" }, @@ -81,7 +88,7 @@ "storybook": { "displayName": "Addon coverage", "supportedFrameworks": [ - "react", "vue", "angular", "web-components", "ember", "html", "svelte", "preact", "react-native" + "react" ], "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" } diff --git a/preset.js b/preset.js index 37cc891..78a914e 100644 --- a/preset.js +++ b/preset.js @@ -6,7 +6,33 @@ function managerEntries(entry = []) { return [...entry, require.resolve("./dist/esm/preset/manager")]; } +const defaultIstanbulOptions = { + cwd: __dirname, + exclude: [ + "**/*.d.ts", + "**/*{.,-}{spec,stories,types}.{js,jsx,ts,tsx}", + ] +} + +const babel = async (babelConfig, options) => { + if (options.useExternalInstrumentation) { + return babelConfig + } + + babelConfig.plugins.push( + [ + "istanbul", + { + ...defaultIstanbulOptions, + ...options.istanbul, + }, + ], + ) + return babelConfig +}; + module.exports = { managerEntries, config, + babel }; diff --git a/src/Panel.tsx b/src/Panel.tsx deleted file mode 100644 index 3f7236f..0000000 --- a/src/Panel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import { useAddonState, useChannel } from "@storybook/api"; -import { AddonPanel } from "@storybook/components"; -import { ADDON_ID, EVENTS } from "./constants"; -import { PanelContent } from "./components/PanelContent"; - -interface PanelProps { - active: boolean; -} - -export const Panel: React.FC = (props) => { - // https://storybook.js.org/docs/react/addons/addons-api#useaddonstate - const [results, setState] = useAddonState(ADDON_ID, { - danger: [], - warning: [], - }); - - // https://storybook.js.org/docs/react/addons/addons-api#usechannel - const emit = useChannel({ - [EVENTS.RESULT]: (newResults) => setState(newResults), - }); - - return ( - - { - emit(EVENTS.REQUEST); - }} - clearData={() => { - emit(EVENTS.CLEAR); - }} - /> - - ); -}; diff --git a/src/Tab.tsx b/src/Tab.tsx deleted file mode 100644 index 1cfc887..0000000 --- a/src/Tab.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import { useParameter } from "@storybook/api"; -import { PARAM_KEY } from "./constants"; -import { TabContent } from "./components/TabContent"; - -interface TabProps { - active: boolean; -} - -export const Tab: React.FC = ({ active }) => { - // https://storybook.js.org/docs/react/addons/addons-api#useparameter - const paramData = useParameter(PARAM_KEY, ""); - - return active ? : null; -}; diff --git a/src/Tool.tsx b/src/Tool.tsx deleted file mode 100644 index c5d39b6..0000000 --- a/src/Tool.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useCallback } from "react"; -import { useGlobals } from "@storybook/api"; -import { Icons, IconButton } from "@storybook/components"; -import { TOOL_ID } from "./constants"; - -export const Tool = () => { - const [{ myAddon }, updateGlobals] = useGlobals(); - - const toggleMyTool = useCallback( - () => - updateGlobals({ - myAddon: myAddon ? undefined : true, - }), - [myAddon] - ); - - return ( - - {/* - Checkout https://next--storybookjs.netlify.app/official-storybook/?path=/story/basics-icon--labels - for the full list of icons - */} - - - ); -}; diff --git a/src/components/CoveragePanel.tsx b/src/components/CoveragePanel.tsx new file mode 100644 index 0000000..0c754a4 --- /dev/null +++ b/src/components/CoveragePanel.tsx @@ -0,0 +1,47 @@ + +import React from 'react'; +import { useChannel, useAddonState } from '@storybook/api'; +import { SyntaxHighlighter } from '@storybook/components'; + +import { ADDON_ID, EVENTS } from '../constants'; +import { CoverageDetail } from '../types'; +import { lineCoverage } from '../utils'; + +interface DetailPanelProps { + detail: CoverageDetail; +} + +const DetailPanel: React.FC = ({ detail }) => { + if (!(detail.source && detail.item)) { + return
No coverage set
; + } + + const lineToMissing = lineCoverage(detail.item); + const lineProps = (lineNumber: number) => + lineToMissing[lineNumber] ? { style: { backgroundColor: '#ffcccc', borderLeft: '5px solid #f85151' } } : { style: { borderLeft: '5px solid #95de95' } }; + return ( + + {detail.source} + + ); +}; + +export const CoveragePanel: React.VFC = () => { + const [coverageDetail, setCoverageDetail] = useAddonState(ADDON_ID, null); + + useChannel({ + [EVENTS.COVERAGE_DETAIL]: (detail: CoverageDetail) => setCoverageDetail(detail), + }); + + return coverageDetail ? :
No coverage
; +}; \ No newline at end of file diff --git a/src/components/List.tsx b/src/components/List.tsx deleted file mode 100644 index aa89621..0000000 --- a/src/components/List.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { Fragment, useState } from "react"; -import { styled, themes, convert } from "@storybook/theming"; -import { Icons, IconsProps } from "@storybook/components"; - -const ListWrapper = styled.ul({ - listStyle: "none", - fontSize: 14, - padding: 0, - margin: 0, -}); - -const Wrapper = styled.div({ - display: "flex", - width: "100%", - borderBottom: `1px solid ${convert(themes.normal).appBorderColor}`, - "&:hover": { - background: convert(themes.normal).background.hoverable, - }, -}); - -const Icon = styled(Icons)({ - height: 10, - width: 10, - minWidth: 10, - color: convert(themes.normal).color.mediumdark, - marginRight: 10, - transition: "transform 0.1s ease-in-out", - alignSelf: "center", - display: "inline-flex", -}); - -const HeaderBar = styled.div({ - padding: convert(themes.normal).layoutMargin, - paddingLeft: convert(themes.normal).layoutMargin - 3, - background: "none", - color: "inherit", - textAlign: "left", - cursor: "pointer", - borderLeft: "3px solid transparent", - width: "100%", - - "&:focus": { - outline: "0 none", - borderLeft: `3px solid ${convert(themes.normal).color.secondary}`, - }, -}); - -const Description = styled.div({ - padding: convert(themes.normal).layoutMargin, - marginBottom: convert(themes.normal).layoutMargin, - fontStyle: "italic", -}); - -type Item = { - title: string; - description: string; -}; - -interface ListItemProps { - item: Item; -} - -export const ListItem: React.FC = ({ item }) => { - const [open, onToggle] = useState(false); - - return ( - - - onToggle(!open)} role="button"> - - {item.title} - - - {open ? {item.description} : null} - - ); -}; - -interface ListProps { - items: Item[]; -} - -export const List: React.FC = ({ items }) => ( - - {items.map((item, idx) => ( - - ))} - -); diff --git a/src/components/PanelContent.tsx b/src/components/PanelContent.tsx deleted file mode 100644 index 9fcfb5f..0000000 --- a/src/components/PanelContent.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { Fragment } from "react"; -import { styled, themes, convert } from "@storybook/theming"; -import { TabsState, Placeholder, Button } from "@storybook/components"; -import { List } from "./List"; - -export const RequestDataButton = styled(Button)({ - marginTop: "1rem", -}); - -type Results = { - danger: any[]; - warning: any[]; -}; - -interface PanelContentProps { - results: Results; - fetchData: () => void; - clearData: () => void; -} - -/** - * Checkout https://github.com/storybookjs/storybook/blob/next/addons/jest/src/components/Panel.tsx - * for a real world example - */ -export const PanelContent: React.FC = ({ - results, - fetchData, - clearData, -}) => ( - -
- - - Addons can gather details about how a story is rendered. This is panel - uses a tab pattern. Click the button below to fetch data for the other - two tabs. - - - - Request data - - - - Clear data - - - -
-
- -
-
- -
-
-); diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx deleted file mode 100644 index 78ee961..0000000 --- a/src/components/TabContent.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { styled } from "@storybook/theming"; -import { Title, Source, Link } from "@storybook/components"; - -const TabWrapper = styled.div(({ theme }) => ({ - background: theme.background.content, - padding: "4rem 20px", - minHeight: "100vh", - boxSizing: "border-box", -})); - -const TabInner = styled.div({ - maxWidth: 768, - marginLeft: "auto", - marginRight: "auto", -}); - -interface TabContentProps { - code: string; -} - -export const TabContent: React.FC = ({ code }) => ( - - - My Addon -

- Your addon can create a custom tab in Storybook. For example, the - official{" "} - - @storybook/addon-docs - {" "} - uses this pattern. -

-

- You have full control over what content is being rendered here. You can - use components from{" "} - - @storybook/components - {" "} - to match the look and feel of Storybook, for example the{" "} - <Source /> component below. Or build a completely - custom UI. -

- -
-
-); diff --git a/src/constants.ts b/src/constants.ts index d7ef8e4..50cfe05 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,11 +1,7 @@ -export const ADDON_ID = "storybook/my-addon"; -export const TOOL_ID = `${ADDON_ID}/tool`; +export const ADDON_ID = "storybook/addon-coverage"; export const PANEL_ID = `${ADDON_ID}/panel`; -export const TAB_ID = `${ADDON_ID}/tab`; -export const PARAM_KEY = `myAddonParameter`; +export const PARAM_KEY = `coverage`; export const EVENTS = { - RESULT: `${ADDON_ID}/result`, - REQUEST: `${ADDON_ID}/request`, - CLEAR: `${ADDON_ID}/clear`, + COVERAGE_DETAIL: `${ADDON_ID}/COVERAGE_DETAIL`, }; diff --git a/src/preset/manager.ts b/src/preset/manager.ts deleted file mode 100644 index 67675d9..0000000 --- a/src/preset/manager.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { addons, types } from "@storybook/addons"; - -import { ADDON_ID, TOOL_ID, PANEL_ID, TAB_ID } from "../constants"; -import { Tool } from "../Tool"; -import { Panel } from "../Panel"; -import { Tab } from "../Tab"; - -// Register the addon -addons.register(ADDON_ID, () => { - // Register the tool - addons.add(TOOL_ID, { - type: types.TOOL, - title: "My addon", - match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), - render: Tool, - }); - - // Register the panel - addons.add(PANEL_ID, { - type: types.PANEL, - title: "My addon", - match: ({ viewMode }) => viewMode === "story", - render: Panel, - }); - - // Register the tab - addons.add(TAB_ID, { - type: types.TAB, - title: "My addon", - //👇 Checks the current route for the story - route: ({ storyId }) => `/myaddon/${storyId}`, - //👇 Shows the Tab UI element in myaddon view mode - match: ({ viewMode }) => viewMode === "myaddon", - render: Tab, - }); -}); diff --git a/src/preset/manager.tsx b/src/preset/manager.tsx new file mode 100644 index 0000000..34261dd --- /dev/null +++ b/src/preset/manager.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { addons, types } from "@storybook/addons"; +import { AddonPanel } from '@storybook/components'; + +import { ADDON_ID, PANEL_ID, PARAM_KEY } from "../constants"; +import { CoveragePanel } from '../components/CoveragePanel'; + +addons.register(ADDON_ID, () => { + addons.add(PANEL_ID, { + type: types.PANEL, + title: 'Coverage', + match: ({ viewMode }) => viewMode === 'story', // todo add type + render: ({ active, key }) => ( + + + + ), + paramKey: PARAM_KEY, + }); +}); diff --git a/src/preset/preview.ts b/src/preset/preview.ts index b2ba734..b3c81b7 100644 --- a/src/preset/preview.ts +++ b/src/preset/preview.ts @@ -1,14 +1,101 @@ -/** - * A decorator is a way to wrap a story in extra “rendering” functionality. Many addons define decorators - * in order to augment stories: - * - with extra rendering - * - gather details about how a story is rendered - * - * When writing stories, decorators are typically used to wrap stories with extra markup or context mocking. - * - * https://storybook.js.org/docs/react/writing-stories/decorators#gatsby-focus-wrapper - */ -import { withGlobals } from "../withGlobals"; -import { withRoundTrip } from "../withRoundTrip"; - -export const decorators = [withGlobals, withRoundTrip]; +import { addons } from "@storybook/addons"; +import { once } from "@storybook/client-logger"; +import { + STORY_RENDERED, + STORY_PREPARED, + SET_CURRENT_STORY, + STORY_CHANGED, + DOCS_RENDERED, + STORY_SPECIFIED, +} from "@storybook/core-events"; +import { EVENTS } from "../constants"; + +let initialValues = {} as Record>; + +const channel = addons.getChannel(); + +// const originalValues = {} as Record; +// const handler: ProxyHandler = { +// get(target, prop) { +// //@ts-ignore +// return target[prop]; +// }, +// set(object, prop, value) { +// console.log("setting", { object, prop, value }); + +// // @ts-ignore +// object[prop] = value; +// return true; +// }, +// }; + +// global.__coverage__ = new Proxy({}, handler); + +const getStoryStore = () => globalThis.__STORYBOOK_PREVIEW__.storyStore; + +const getCoverage = () => { + if (!globalThis.__coverage__) { + once.warn("Trying to access coverage but it does not exist."); + } + + return globalThis.__coverage__ || {}; +}; + +channel.on(STORY_RENDERED, async (storyId) => { + const currentStory = await getStoryStore().loadStory({ storyId }); + console.log("story is rendered!", storyId); + const { coverage } = currentStory.parameters; + + if (coverage) { + channel.emit(EVENTS.COVERAGE_DETAIL, { + source: coverage.source, + item: getCoverage()[coverage.filePath], + }); + } +}); + +channel.on(STORY_PREPARED, (storyId) => { + console.log("story is prepped!", storyId); +}); + +channel.on(STORY_CHANGED, (storyId) => { + console.log("story is changed!", storyId); +}); + +channel.on(DOCS_RENDERED, (storyId) => { + console.log("docs is rendered!", storyId); +}); + +channel.on(SET_CURRENT_STORY, async ({ storyId, viewMode }) => { + console.log("story is set! time to reset stuff for ", storyId, viewMode); + const currentStory = await getStoryStore().loadStory({ storyId }); + const { coverage } = currentStory.parameters; + if (coverage) { + if (initialValues[coverage.filePath]) { + getCoverage()[coverage.filePath].s = { + ...initialValues[coverage.filePath], + }; + } + } +}); + +channel.on(STORY_SPECIFIED, async ({ storyId, viewMode }) => { + console.log("story is specified!", storyId, viewMode); + const currentStory = await getStoryStore().loadStory({ storyId }); + const { coverage } = currentStory.parameters; + if (coverage) { + // Once Storybook bootstraps, store "original" values + // so we can reset them before rendering a story + Object.keys(getCoverage()).forEach((filePath) => { + if (!initialValues[filePath]) { + initialValues[filePath] = { + ...getCoverage()[filePath].s, + }; + } else { + getCoverage()[filePath].s = { + ...initialValues[filePath], + }; + } + }); + } +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..e4e9b33 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,50 @@ +export interface Start { + line: number; + column: number; +} + +export interface End { + line: number; + column: number; +} + +export interface CodeLocation { + line: number; + column: number; +} + +export interface Location { + start: CodeLocation; + end: CodeLocation; +} +export interface FunctionCoverage { + name: string; + decl: Location; + loc: Location; + line: number; +} + +export interface Branch { + loc: Location; + // expression types like 'cond-expr' and 'binary-expr' + type: string; + locations: Location[]; + line: number; +} + +export interface CoverageItem { + path: string; + statementMap: Record; + fnMap: Record; + branchMap: Record; + s: Record; + f: Record; + b: Record; + _coverageSchema: string; + hash: string; +} + +export interface CoverageDetail { + source: string; + item: CoverageItem; +} diff --git a/src/typings.d.ts b/src/typings.d.ts index 3563aeb..691b719 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1 +1,7 @@ +import { PreviewWeb } from "@storybook/preview-web"; declare module "global"; + +export declare global { + var __STORYBOOK_PREVIEW__: PreviewWeb; + var __coverage__: Record; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c2bd5d5 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,15 @@ +import { CoverageItem } from "./types"; + +export function lineCoverage(item: CoverageItem) { + const lineToMissing: Record = {}; + Object.entries(item.s).forEach(([statementId, isCovered]) => { + const stmt = item.statementMap[statementId]; + if (!isCovered) { + for (let i: number = stmt.start.line; i <= stmt.end.line; i += 1) { + lineToMissing[i] = true; + } + } + }); + + return lineToMissing; +} diff --git a/src/withGlobals.ts b/src/withGlobals.ts deleted file mode 100644 index 6b491c3..0000000 --- a/src/withGlobals.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { DecoratorFunction } from "@storybook/addons"; -import { useEffect, useGlobals } from "@storybook/addons"; - -export const withGlobals: DecoratorFunction = (StoryFn, context) => { - const [{ myAddon }] = useGlobals(); - // Is the addon being used in the docs panel - const isInDocs = context.viewMode === "docs"; - - useEffect(() => { - // Execute your side effect here - // For example, to manipulate the contents of the preview - const selectorId = isInDocs - ? `#anchor--${context.id} .docs-story` - : `#root`; - - displayToolState(selectorId, { - myAddon, - isInDocs, - }); - }, [myAddon]); - - return StoryFn(); -}; - -function displayToolState(selector: string, state: any) { - const rootElement = document.querySelector(selector); - let preElement = rootElement.querySelector("pre"); - - if (!preElement) { - preElement = document.createElement("pre"); - preElement.style.setProperty("margin-top", "2rem"); - preElement.style.setProperty("padding", "1rem"); - preElement.style.setProperty("background-color", "#eee"); - preElement.style.setProperty("border-radius", "3px"); - preElement.style.setProperty("max-width", "600px"); - rootElement.appendChild(preElement); - } - - preElement.innerText = `This snippet is injected by the withGlobals decorator. -It updates as the user interacts with the ⚡ tool in the toolbar above. - -${JSON.stringify(state, null, 2)} -`; -} diff --git a/src/withRoundTrip.ts b/src/withRoundTrip.ts deleted file mode 100644 index 725948c..0000000 --- a/src/withRoundTrip.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useChannel } from "@storybook/addons"; -import type { DecoratorFunction } from "@storybook/addons"; -import { STORY_CHANGED } from "@storybook/core-events"; -import { EVENTS } from "./constants"; - -export const withRoundTrip: DecoratorFunction = (storyFn) => { - const emit = useChannel({ - [EVENTS.REQUEST]: () => { - emit(EVENTS.RESULT, { - danger: [ - { - title: "Panels are the most common type of addon in the ecosystem", - description: - "For example the official @storybook/actions and @storybook/a11y use this pattern", - }, - { - title: - "You can specify a custom title for your addon panel and have full control over what content it renders", - description: - "@storybook/components offers components to help you addons with the look and feel of Storybook itself", - }, - ], - warning: [ - { - title: - 'This tabbed UI pattern is a popular option to display "test" reports.', - description: - "It's used by @storybook/addon-jest and @storybook/addon-a11y. @storybook/components offers this and other components to help you quickly build an addon", - }, - ], - }); - }, - [STORY_CHANGED]: () => { - emit(EVENTS.RESULT, { - danger: [], - warning: [], - }); - }, - [EVENTS.CLEAR]: () => { - emit(EVENTS.RESULT, { - danger: [], - warning: [], - }); - }, - }); - return storyFn(); -}; diff --git a/stories/Button.js b/stories/Button.js index 19c9656..c6350b3 100644 --- a/stories/Button.js +++ b/stories/Button.js @@ -5,10 +5,15 @@ import "./button.css"; /** * Primary UI component for user interaction */ -export const Button = ({ primary, backgroundColor, size, label, ...props }) => { +export const Button = ({ primary, loading, backgroundColor, size, label, ...props }) => { const mode = primary ? "storybook-button--primary" : "storybook-button--secondary"; + + if (loading) { + return
Loading...
+ } + return ( } +