├── packages ├── addon │ ├── index.js │ ├── index.mjs │ ├── .eslintrc.js │ ├── preview.js │ ├── register.js │ ├── preview.mjs │ ├── NOTES.md │ ├── tsconfig.build.cjs.json │ ├── tsconfig.build.esm.json │ ├── vitest.config.mts │ ├── LICENSE.md │ ├── src │ │ ├── preview.ts │ │ └── utils │ │ │ ├── general.ts │ │ │ └── general.test.ts │ ├── index.d.ts │ └── package.json ├── example-v7-webpack │ ├── src │ │ ├── tests │ │ │ ├── utils │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ └── StorybookPage.ts │ │ │ ├── WithTypedProps.spec.ts │ │ │ └── types-test.ts │ │ └── stories │ │ │ ├── Basic.tsx │ │ │ ├── Dev.tsx │ │ │ ├── WithTypedProps.tsx │ │ │ ├── Basic.stories.ts │ │ │ ├── WithTypedProps.stories.tsx │ │ │ └── Dev.stories.ts │ ├── .storybook │ │ ├── manager.ts │ │ └── main.ts │ ├── nodemon.json │ ├── tsconfig.json │ ├── package.json │ └── playwright.config.ts ├── example-v8-generic │ ├── src │ │ ├── stories │ │ │ ├── Basic.tsx │ │ │ ├── Dev.tsx │ │ │ ├── WithAutoDocs.tsx │ │ │ ├── WithTypedProps.tsx │ │ │ ├── Basic.stories.ts │ │ │ ├── WithAutoDocs.stories.tsx │ │ │ ├── WithTypedProps.stories.tsx │ │ │ ├── WithAutoDocs.spec.ts │ │ │ ├── WithTypedProps.spec.ts │ │ │ └── Dev.stories.tsx │ │ ├── utils │ │ │ ├── escapeRegExp.ts │ │ │ ├── index.ts │ │ │ └── constants.ts │ │ └── tests │ │ │ ├── types.ts │ │ │ └── objects │ │ │ ├── DocsPageObject.ts │ │ │ ├── StoryPageObject.ts │ │ │ └── AppObject.ts │ ├── tsconfig.json │ ├── global-setup.ts │ ├── package.json │ ├── README.md │ └── playwright.config.ts ├── example-v10-vite │ ├── .storybook │ │ ├── manager.ts │ │ └── main.ts │ ├── nodemon.json │ └── package.json ├── example-v8-vite │ ├── .storybook │ │ ├── manager.ts │ │ └── main.ts │ ├── nodemon.json │ └── package.json ├── example-v8-webpack │ ├── .storybook │ │ ├── manager.ts │ │ └── main.ts │ ├── nodemon.json │ └── package.json ├── example-v9-vite │ ├── .storybook │ │ ├── manager.ts │ │ └── main.ts │ ├── nodemon.json │ └── package.json ├── common │ ├── package.json │ └── src │ │ └── index.ts ├── example-v10-generic │ ├── tsconfig.json │ ├── global-setup.ts │ ├── src │ │ ├── stories │ │ │ ├── Dev.tsx │ │ │ ├── Basic.tsx │ │ │ ├── WithAutoDocs.tsx │ │ │ ├── WithTypedProps.tsx │ │ │ ├── WithAutoDocs.stories.tsx │ │ │ ├── Basic.stories.ts │ │ │ ├── WithTypedProps.stories.tsx │ │ │ ├── WithAutoDocs.spec.ts │ │ │ ├── WithTypedProps.spec.ts │ │ │ └── Dev.stories.tsx │ │ ├── utils │ │ │ ├── escapeRegExp.ts │ │ │ ├── index.ts │ │ │ └── constants.ts │ │ └── tests │ │ │ ├── types.ts │ │ │ ├── objects │ │ │ ├── DocsPageObject.ts │ │ │ ├── StoryPageObject.ts │ │ │ └── AppObject.ts │ │ │ └── types-test.ts │ ├── package.json │ ├── README.md │ └── playwright.config.ts ├── example-v9-generic │ ├── tsconfig.json │ ├── global-setup.ts │ ├── src │ │ ├── stories │ │ │ ├── Basic.tsx │ │ │ ├── Dev.tsx │ │ │ ├── WithAutoDocs.tsx │ │ │ ├── WithTypedProps.tsx │ │ │ ├── WithAutoDocs.stories.tsx │ │ │ ├── Basic.stories.ts │ │ │ ├── WithTypedProps.stories.tsx │ │ │ ├── WithAutoDocs.spec.ts │ │ │ ├── WithTypedProps.spec.ts │ │ │ └── Dev.stories.tsx │ │ ├── utils │ │ │ ├── escapeRegExp.ts │ │ │ ├── index.ts │ │ │ └── constants.ts │ │ └── tests │ │ │ ├── types.ts │ │ │ └── objects │ │ │ ├── DocsPageObject.ts │ │ │ ├── StoryPageObject.ts │ │ │ └── AppObject.ts │ ├── package.json │ ├── README.md │ └── playwright.config.ts └── example-prod │ ├── NOTES.md │ ├── tsconfig.json │ ├── src │ └── stories │ │ ├── Dev.tsx │ │ ├── Basic.tsx │ │ ├── WithAutoDocs.tsx │ │ ├── WithTypedProps.tsx │ │ ├── WithAutoDocs.stories.tsx │ │ ├── Basic.stories.ts │ │ ├── WithTypedProps.stories.tsx │ │ └── Dev.stories.tsx │ ├── package.json │ ├── yarn.lock │ └── .storybook │ └── main.ts ├── .eslintignore ├── prettier.config.js ├── .gitattributes ├── public └── media │ ├── complex-example.gif │ ├── example-with-arrays.png │ ├── control-matcher-result.png │ ├── simple-example-with-addon.png │ ├── example-with-customised-docs.png │ ├── simple-example-without-addon.png │ └── simple-example-with-custom-control.png ├── .yarnrc.yml ├── .eslintrc.js ├── tsconfig.json ├── CHANGELOG.md ├── LICENSE ├── netlify.toml ├── .github ├── LICENSE.md └── workflows │ └── pr-check.yaml ├── scripts └── sync-github-files.js ├── CONTRIBUTING.md ├── package.json └── .gitignore /packages/addon/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/addon/index.mjs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/**/* 2 | **/node_modules/**/* -------------------------------------------------------------------------------- /packages/addon/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/addon/preview.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/cjs/preview"); 2 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@eliasm307/config/prettier")(); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /packages/addon/register.js: -------------------------------------------------------------------------------- 1 | // required for react-native apparently, even if it is empty 2 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/tests/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const STORYBOOK_V7_PORT = 6007; 2 | -------------------------------------------------------------------------------- /packages/addon/preview.mjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-restricted-exports 2 | export {default} from "./dist/esm/preview.js"; 3 | -------------------------------------------------------------------------------- /public/media/complex-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/complex-example.gif -------------------------------------------------------------------------------- /public/media/example-with-arrays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/example-with-arrays.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: 0 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 8 | -------------------------------------------------------------------------------- /public/media/control-matcher-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/control-matcher-result.png -------------------------------------------------------------------------------- /public/media/simple-example-with-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/simple-example-with-addon.png -------------------------------------------------------------------------------- /public/media/example-with-customised-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/example-with-customised-docs.png -------------------------------------------------------------------------------- /public/media/simple-example-without-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/simple-example-without-addon.png -------------------------------------------------------------------------------- /public/media/simple-example-with-custom-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasm307/storybook-addon-deep-controls/HEAD/public/media/simple-example-with-custom-control.png -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Basic({}: {deep: {bool: boolean}; num: number}) { 4 | return
Basic
; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Basic({}: {deep: {bool: boolean}; num: number}) { 4 | return
Basic
; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example-v10-vite/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from "storybook/manager-api"; 2 | 3 | // see https://storybook.js.org/docs/react/configure/features-and-behavior 4 | addons.setConfig({ 5 | showPanel: true, 6 | panelPosition: "right", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from "@storybook/manager-api"; 2 | 3 | // see https://storybook.js.org/docs/react/configure/features-and-behavior 4 | addons.setConfig({ 5 | showPanel: true, 6 | panelPosition: "right", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/example-v8-vite/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from "@storybook/manager-api"; 2 | 3 | // see https://storybook.js.org/docs/react/configure/features-and-behavior 4 | addons.setConfig({ 5 | showPanel: true, 6 | panelPosition: "right", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/example-v8-webpack/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from "@storybook/manager-api"; 2 | 3 | // see https://storybook.js.org/docs/react/configure/features-and-behavior 4 | addons.setConfig({ 5 | showPanel: true, 6 | panelPosition: "right", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/example-v9-vite/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from "storybook/manager-api"; 2 | 3 | // see https://storybook.js.org/docs/react/configure/features-and-behavior 4 | addons.setConfig({ 5 | showPanel: true, 6 | panelPosition: "right", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storybook-addon-deep-controls/common-internal", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "main": "src/index.ts", 8 | "module": "src/index.ts" 9 | } 10 | -------------------------------------------------------------------------------- /packages/example-v10-generic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v8-generic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v9-generic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/addon/NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | - Writing a preset addon: https://storybook.js.org/docs/react/addons/writing-presets 4 | - Writing an addon: https://storybook.js.org/docs/react/addons/writing-addons 5 | - Addon template: https://github.com/storybookjs/addon-kit/blob/main/package.json 6 | 7 | ## Todo 8 | 9 | - Remove v7 example and support 10 | -------------------------------------------------------------------------------- /packages/example-v10-generic/global-setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {type FullConfig} from "@playwright/test"; 3 | import {CONFIG} from "./src/utils/constants"; 4 | 5 | export default function globalSetup(config: FullConfig) { 6 | console.log("globalSetup env", CONFIG); 7 | console.log("globalSetup webServer", config.webServer); 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-v8-generic/global-setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {type FullConfig} from "@playwright/test"; 3 | import {CONFIG} from "./src/utils/constants"; 4 | 5 | export default function globalSetup(config: FullConfig) { 6 | console.log("globalSetup env", CONFIG); 7 | console.log("globalSetup webServer", config.webServer); 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-v9-generic/global-setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {type FullConfig} from "@playwright/test"; 3 | import {CONFIG} from "./src/utils/constants"; 4 | 5 | export default function globalSetup(config: FullConfig) { 6 | console.log("globalSetup env", CONFIG); 7 | console.log("globalSetup webServer", config.webServer); 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-v10-vite/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "delay": 5000, 4 | "colours": true, 5 | "verbose": true, 6 | "exec": "npm run storybook", 7 | "watch": ["./.storybook", "../addon"], 8 | "ext": "ts,tsx,js,jsx,json", 9 | "ignore": ["**/dist/", "**/coverage/", "**/node_modules/", "**/*.test.*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "delay": 5000, 4 | "colours": true, 5 | "verbose": true, 6 | "exec": "npm run storybook", 7 | "watch": ["./.storybook", "../addon"], 8 | "ext": "ts,tsx,js,jsx,json", 9 | "ignore": ["**/dist/", "**/coverage/", "**/node_modules/", "**/*.test.*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v8-vite/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "delay": 5000, 4 | "colours": true, 5 | "verbose": true, 6 | "exec": "npm run storybook", 7 | "watch": ["./.storybook", "../addon"], 8 | "ext": "ts,tsx,js,jsx,json", 9 | "ignore": ["**/dist/", "**/coverage/", "**/node_modules/", "**/*.test.*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v8-webpack/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "delay": 5000, 4 | "colours": true, 5 | "verbose": true, 6 | "exec": "npm run storybook", 7 | "watch": ["./.storybook", "../addon"], 8 | "ext": "ts,tsx,js,jsx,json", 9 | "ignore": ["**/dist/", "**/coverage/", "**/node_modules/", "**/*.test.*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-v9-vite/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "delay": 5000, 4 | "colours": true, 5 | "verbose": true, 6 | "exec": "npm run storybook", 7 | "watch": ["./.storybook", "../addon"], 8 | "ext": "ts,tsx,js,jsx,json", 9 | "ignore": ["**/dist/", "**/coverage/", "**/node_modules/", "**/*.test.*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/example-prod/NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | - Need to have this outside the workspaces directory so yarn doesnt use a link to the local version, it seems to ignore the version in the package.json 4 | - Also dont need other dependencies as this will get them from the parent node_modules implicitly, so we need this to be in a lower directory than the workspaces root with the node_modules 5 | -------------------------------------------------------------------------------- /packages/example-prod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve", 7 | "plugins": [ 8 | { 9 | "name": "next" 10 | } 11 | ] 12 | }, 13 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/addon/tsconfig.build.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs", 5 | "noEmit": false, 6 | "incremental": false, 7 | "stripInternal": true, 8 | "target": "ES2020", 9 | "module": "Node16", 10 | "moduleResolution": "Node16" 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/addon/tsconfig.build.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/esm", 5 | "noEmit": false, 6 | "incremental": false, 7 | "stripInternal": true, 8 | "target": "ES2020", 9 | "module": "esnext", 10 | "moduleResolution": "Bundler" 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/addon/vitest.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import {defineConfig} from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | chaiConfig: { 7 | showDiff: true, 8 | truncateThreshold: 0, 9 | }, 10 | coverage: { 11 | enabled: true, 12 | exclude: ["**/*.d.ts", "**/*.js", "**/*.test.ts", "**/*.spec.ts"], 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve", 7 | "plugins": [ 8 | { 9 | "name": "next" 10 | } 11 | ] 12 | }, 13 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/Dev.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | export default function Dev(config: object) { 4 | return ( 5 |
6 |

Config received by Component:

7 |
14 |         {stringify(config)}
15 |       
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/Dev.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Basic(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/WithAutoDocs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/Dev.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/Dev.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Basic(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/Dev.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Basic(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithAutoDocs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithAutoDocs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithAutoDocs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 3 | 4 | export default function Dev(config: object) { 5 | return ( 6 |
7 |

Config received by Component:

8 |
15 |         {stringify(config)}
16 |       
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const ecmConfig = require("@eliasm307/config/eslint")({withPrettier: true, withReact: false}); 2 | 3 | module.exports = { 4 | ...ecmConfig, 5 | root: true, 6 | extends: [...ecmConfig.extends, "plugin:storybook/recommended"], 7 | // plugins: [...ecmConfig.plugins].filter((plugin) => plugin !== "react-hooks"), 8 | rules: { 9 | ...ecmConfig.rules, 10 | "import/prefer-default-export": "off", 11 | }, 12 | overrides: [ 13 | ...ecmConfig.overrides, 14 | { 15 | files: ["*.ts", "*.tsx"], 16 | rules: {}, 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from "lodash/cloneDeep"; 2 | 3 | export function clone>(obj: T): T { 4 | return cloneDeep(obj); // maintains value as is, e.g. NaN, Infinity, etc. which JSON.stringify does not 5 | } 6 | 7 | export function localHostPortIsInUse(port: number): Promise { 8 | return new Promise((resolve) => { 9 | const server = require("http").createServer(); 10 | server.on("error", () => { 11 | server.close(); 12 | resolve(true); 13 | }); 14 | server.on("listening", () => { 15 | server.close(); 16 | resolve(false); 17 | }); 18 | server.listen(port); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/example-v9-generic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v9-generic", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "playwright": "node ../../node_modules/playwright/cli.js", 9 | "playwright:vite": "cross-env STORYBOOK_EXAMPLE_TYPE=vite npm run playwright -- ", 10 | "playwright:webpack": "cross-env STORYBOOK_EXAMPLE_TYPE=webpack npm run playwright -- " 11 | }, 12 | "devDependencies": { 13 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 14 | "@storybook/react": "9.0.12", 15 | "cross-env": "7.0.3", 16 | "storybook": "9.0.12", 17 | "storybook-addon-deep-controls": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v10-generic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v10-generic", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "playwright": "node ../../node_modules/playwright/cli.js", 9 | "playwright:vite": "cross-env STORYBOOK_EXAMPLE_TYPE=vite npm run playwright -- ", 10 | "playwright:webpack": "cross-env STORYBOOK_EXAMPLE_TYPE=webpack npm run playwright -- " 11 | }, 12 | "devDependencies": { 13 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 14 | "@storybook/react": "10.0.2", 15 | "cross-env": "7.0.3", 16 | "storybook": "10.0.2", 17 | "storybook-addon-deep-controls": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v8-generic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v8-generic", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "playwright": "node ../../node_modules/playwright/cli.js", 9 | "playwright:v8-vite": "cross-env STORYBOOK_EXAMPLE_TYPE=v8-vite npm run playwright", 10 | "playwright:v8-webpack": "cross-env STORYBOOK_EXAMPLE_TYPE=v8-webpack npm run playwright" 11 | }, 12 | "devDependencies": { 13 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 14 | "@storybook/react": "8.3.6", 15 | "cross-env": "7.0.3", 16 | "storybook": "8.3.6", 17 | "storybook-addon-deep-controls": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-v8-generic/README.md: -------------------------------------------------------------------------------- 1 | # Storybook V8 Generic Example 2 | 3 | This package is meant to contain generic stories and tests for Storybook V8. 4 | 5 | Specific configurations for Storybook V8 can be defined as separate packages that use the stories in this package (ie all v8 packages have the same stories). 6 | 7 | Tests in this package are triggered from the relevant v8 package and set env variables which define which v8 package and URL the Playwright tests should run against. 8 | 9 | The benefit of this approach is that we can have a single package with all the stories and tests for a specific version of Storybook, and then have multiple packages that define various configurations that should have the same behavior in terms of the deep controls addon. 10 | -------------------------------------------------------------------------------- /packages/example-v9-generic/README.md: -------------------------------------------------------------------------------- 1 | # Storybook V9 Generic Example 2 | 3 | This package is meant to contain generic stories and tests for Storybook V9. 4 | 5 | Specific configurations for this Storybook version can be defined as separate packages that use the stories in this package (ie all packages have the same stories). 6 | 7 | Tests in this package are triggered from the relevant package and set env variables which define which package and URL the Playwright tests should run against. 8 | 9 | The benefit of this approach is that we can have a single package with all the stories and tests for a specific version of Storybook, and then have multiple packages that define various configurations that should have the same behavior in terms of the deep controls addon. 10 | -------------------------------------------------------------------------------- /packages/example-v10-generic/README.md: -------------------------------------------------------------------------------- 1 | # Storybook V10 Generic Example 2 | 3 | This package is meant to contain generic stories and tests for Storybook V10. 4 | 5 | Specific configurations for this Storybook version can be defined as separate packages that use the stories in this package (ie all packages have the same stories). 6 | 7 | Tests in this package are triggered from the relevant package and set env variables which define which package and URL the Playwright tests should run against. 8 | 9 | The benefit of this approach is that we can have a single package with all the stories and tests for a specific version of Storybook, and then have multiple packages that define various configurations that should have the same behavior in terms of the deep controls addon. 10 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/WithTypedProps.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | // NOTE: the docs addon will try to create argTypes for this component's props based on this types that this addon needs to handle 4 | type Props = { 5 | someString?: string; 6 | someObject?: { 7 | anyString: string; 8 | enumString: string; 9 | }; 10 | someArray?: string[]; 11 | }; 12 | 13 | export default function WithTypedProps(config: Props) { 14 | return ( 15 |
16 |

Config received by Component:

17 |
24 |         {stringify(config)}
25 |       
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithTypedProps.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | // NOTE: the docs addon will try to create argTypes for this component's props based on this types that this addon needs to handle 4 | type Props = { 5 | someString?: string; 6 | someObject?: { 7 | anyString: string; 8 | enumString: string; 9 | }; 10 | someArray?: string[]; 11 | }; 12 | 13 | export default function WithTypedProps(config: Props) { 14 | return ( 15 |
16 |

Config received by Component:

17 |
24 |         {stringify(config)}
25 |       
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/WithTypedProps.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | // NOTE: the docs addon will try to create argTypes for this component's props based on this types that this addon needs to handle 4 | type Props = { 5 | someString?: string; 6 | someObject?: { 7 | anyString: string; 8 | enumString: string; 9 | }; 10 | someArray?: string[]; 11 | }; 12 | 13 | export default function WithTypedProps(config: Props) { 14 | return ( 15 |
16 |

Config received by Component:

17 |
24 |         {stringify(config)}
25 |       
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithTypedProps.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | // NOTE: the docs addon will try to create argTypes for this component's props based on this types that this addon needs to handle 4 | type Props = { 5 | someString?: string; 6 | someObject?: { 7 | anyString: string; 8 | enumString: string; 9 | }; 10 | someArray?: string[]; 11 | }; 12 | 13 | export default function WithTypedProps(config: Props) { 14 | return ( 15 |
16 |

Config received by Component:

17 |
24 |         {stringify(config)}
25 |       
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithTypedProps.tsx: -------------------------------------------------------------------------------- 1 | import {stringify} from "@storybook-addon-deep-controls/common-internal"; 2 | 3 | // NOTE: the docs addon will try to create argTypes for this component's props based on this types that this addon needs to handle 4 | type Props = { 5 | someString?: string; 6 | someObject?: { 7 | anyString: string; 8 | enumString: string; 9 | }; 10 | someArray?: string[]; 11 | }; 12 | 13 | export default function WithTypedProps(config: Props) { 14 | return ( 15 |
16 |

Config received by Component:

17 |
24 |         {stringify(config)}
25 |       
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-prod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app-prod", 3 | "private": true, 4 | "scripts": { 5 | "yarn": "yarn", 6 | "upgrade": "yarn upgrade-interactive", 7 | "storybook": "echo 'Starting storybook' && storybook dev -p 6006 --no-open", 8 | "build": "storybook build" 9 | }, 10 | "installConfig": { 11 | "hoistingLimits": "workspaces" 12 | }, 13 | "dependencies": { 14 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 15 | "@storybook/addon-docs": "9.0.12", 16 | "@storybook/builder-vite": "9.0.12", 17 | "@storybook/react": "9.0.12", 18 | "@storybook/react-vite": "9.0.12", 19 | "@vitejs/plugin-react": "4.5.2", 20 | "cross-env": "7.0.3", 21 | "nodemon": "3.1.10", 22 | "storybook": "9.0.12", 23 | "storybook-addon-deep-controls": "^0.9.4", 24 | "vite": "6.3.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noImplicitAny": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitOverride": true, 13 | "noImplicitThis": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "incremental": true, 21 | "jsx": "preserve" 22 | }, 23 | "exclude": [ 24 | "**/node_modules/**/*", 25 | "**/dist/**/*", 26 | "**/coverage/**/*", 27 | "**/storybook-static/**/*", 28 | "**/public/**/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type {StorybookConfig} from "@storybook/nextjs"; 2 | import {join, dirname} from "path"; 3 | 4 | /** 5 | * This function is used to resolve the absolute path of a package. 6 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 7 | */ 8 | function getAbsolutePathToPackage(value: string): any { 9 | return dirname(require.resolve(join(value, "package.json"))); 10 | } 11 | const config: StorybookConfig = { 12 | stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 13 | addons: [ 14 | "@storybook/addon-essentials", // to get controls and docs addons and make sure we are compatible with any other essential addon 15 | "storybook-addon-deep-controls", 16 | ].map(getAbsolutePathToPackage), 17 | framework: { 18 | name: getAbsolutePathToPackage("@storybook/nextjs"), 19 | options: {}, 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/Basic.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Basic from "./Basic"; 4 | 5 | const meta = { 6 | component: Basic, 7 | parameters: { 8 | deepControls: { 9 | enabled: true, 10 | }, 11 | }, 12 | } satisfies TypeWithDeepControls>; 13 | 14 | export default meta; 15 | 16 | type Story = TypeWithDeepControls>; 17 | 18 | export const Enabled: Story = { 19 | args: { 20 | deep: { 21 | bool: true, 22 | }, 23 | num: 1, 24 | }, 25 | argTypes: { 26 | "deep.bool": { 27 | control: "boolean", 28 | }, 29 | }, 30 | }; 31 | 32 | export const WithoutInitialArgTypes: Story = { 33 | args: { 34 | deep: { 35 | bool: true, 36 | }, 37 | num: 1, 38 | }, 39 | argTypes: {}, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/example-prod/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 6 6 | cacheKey: 8c0 7 | 8 | "example-app-prod@workspace:.": 9 | version: 0.0.0-use.local 10 | resolution: "example-app-prod@workspace:." 11 | dependencies: 12 | storybook-addon-deep-controls: ^0.8.1 13 | languageName: unknown 14 | linkType: soft 15 | 16 | "storybook-addon-deep-controls@npm:^0.8.1": 17 | version: 0.8.1 18 | resolution: "storybook-addon-deep-controls@npm:0.8.1" 19 | peerDependencies: 20 | "@storybook/addon-controls": 7.x.x || 8.x.x 21 | "@storybook/types": 7.x.x || 8.x.x 22 | storybook: 7.x.x || 8.x.x 23 | checksum: cc0b0f749e38e731cfc939eb0fe9f333ee68e0ad52ec5482ab15edad6a5a5ac4c2cb0dd33cb96ddbc87543005ac4d3096793fea6c5ca4b13adb358d6cb449e65 24 | languageName: node 25 | linkType: hard 26 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/Basic.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Basic from "./Basic"; 4 | 5 | const meta = { 6 | component: Basic, 7 | parameters: { 8 | deepControls: { 9 | enabled: true, 10 | }, 11 | }, 12 | argTypes: { 13 | "deep.bool": { 14 | control: "boolean", 15 | }, 16 | // @ts-expect-error [values are typed] 17 | foo: 1, 18 | }, 19 | } satisfies TypeWithDeepControls>; 20 | 21 | export default meta; 22 | 23 | type Story = TypeWithDeepControls>; 24 | 25 | export const Enabled: Story = { 26 | args: { 27 | deep: { 28 | bool: true, 29 | }, 30 | num: 1, 31 | }, 32 | argTypes: { 33 | "deep.bool": { 34 | control: "boolean", 35 | }, 36 | // @ts-expect-error [values are typed] 37 | foo: 1, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/utils/escapeRegExp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to match `RegExp` 3 | * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). 4 | * 5 | * @link https://github.com/lodash/lodash/blob/master/escapeRegExp.js 6 | */ 7 | const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; 8 | const reHasRegExpChar = RegExp(reRegExpChar.source); 9 | 10 | /** 11 | * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", 12 | * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. 13 | * 14 | * @since 3.0.0 15 | * @category String 16 | * @param {string} [string=''] The string to escape. 17 | * @returns {string} Returns the escaped string. 18 | * @see escape, escapeRegExp, unescape 19 | * @example 20 | * 21 | * escapeRegExp('[lodash](https://lodash.com/)') 22 | * // => '\[lodash\]\(https://lodash\.com/\)' 23 | */ 24 | export default function escapeRegExp(string: string): string { 25 | return string && reHasRegExpChar.test(string) 26 | ? string.replace(reRegExpChar, "\\$&") 27 | : string || ""; 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v8-webpack/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type {StorybookConfig} from "@storybook/nextjs"; 2 | import {dirname, join} from "path"; 3 | 4 | // NOTE: need to ues this instead of passing package names directly 5 | // as that would change how the json control renders function values for some reason 6 | function getAbsolutePathToPackage(value: string): any { 7 | return dirname(require.resolve(join(value, "package.json"))); 8 | } 9 | 10 | const config: StorybookConfig = { 11 | stories: ["../../example-v8-generic/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 12 | addons: [ 13 | "@storybook/addon-essentials", // to get controls and docs addons and make sure we are compatible with any other essential addon 14 | "storybook-addon-deep-controls", 15 | ].map(getAbsolutePathToPackage), 16 | framework: { 17 | name: getAbsolutePathToPackage("@storybook/nextjs"), 18 | options: { 19 | builder: { 20 | fsCache: true, 21 | lazyCompilation: true, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/utils/escapeRegExp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to match `RegExp` 3 | * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). 4 | * 5 | * @link https://github.com/lodash/lodash/blob/master/escapeRegExp.js 6 | */ 7 | const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; 8 | const reHasRegExpChar = RegExp(reRegExpChar.source); 9 | 10 | /** 11 | * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", 12 | * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. 13 | * 14 | * @since 3.0.0 15 | * @category String 16 | * @param {string} [string=''] The string to escape. 17 | * @returns {string} Returns the escaped string. 18 | * @see escape, escapeRegExp, unescape 19 | * @example 20 | * 21 | * escapeRegExp('[lodash](https://lodash.com/)') 22 | * // => '\[lodash\]\(https://lodash\.com/\)' 23 | */ 24 | export default function escapeRegExp(string: string): string { 25 | return string && reHasRegExpChar.test(string) 26 | ? string.replace(reRegExpChar, "\\$&") 27 | : string || ""; 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/utils/escapeRegExp.ts: -------------------------------------------------------------------------------- 1 | // todo move to a common location 2 | 3 | /** 4 | * Used to match `RegExp` 5 | * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). 6 | * 7 | * @link https://github.com/lodash/lodash/blob/master/escapeRegExp.js 8 | */ 9 | const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; 10 | const reHasRegExpChar = RegExp(reRegExpChar.source); 11 | 12 | /** 13 | * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", 14 | * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. 15 | * 16 | * @since 3.0.0 17 | * @category String 18 | * @param {string} [string=''] The string to escape. 19 | * @returns {string} Returns the escaped string. 20 | * @see escape, escapeRegExp, unescape 21 | * @example 22 | * 23 | * escapeRegExp('[lodash](https://lodash.com/)') 24 | * // => '\[lodash\]\(https://lodash\.com/\)' 25 | */ 26 | export default function escapeRegExp(string: string): string { 27 | return string && reHasRegExpChar.test(string) 28 | ? string.replace(reRegExpChar, "\\$&") 29 | : string || ""; 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.0 2 | 3 | - Add `storybook` v10 to supported versions and add tests, to resolve [#68](https://github.com/eliasm307/storybook-addon-deep-controls/issues/68) 4 | - Update module to esnext and moduleResolution to Bundler for ESM build tsconfig.json 5 | - Update package.json exports to default to ESM 6 | 7 | ## 0.9.5 8 | 9 | - Update `storybook` peer dependency version range to prevent `npm install` error, to resolve [#66](https://github.com/eliasm307/storybook-addon-deep-controls/issues/66) 10 | - Update Readme documentation 11 | 12 | ## 0.9.4 13 | 14 | - Add `storybook` v9 to supported versions and add tests, to resolve [#52](https://github.com/eliasm307/storybook-addon-deep-controls/issues/52) 15 | 16 | ## 0.9.3 17 | 18 | - Update `PartialStrictInputType` to resolve [#54](https://github.com/eliasm307/storybook-addon-deep-controls/issues/54) 19 | 20 | ## 0.9.2 21 | 22 | - Set tsconfig.json build target to `ES2020` 23 | 24 | ## 0.9.1 25 | 26 | - Define `package.json` exports 27 | - Add vite builder support and tests 28 | 29 | ## 0.9.0 30 | 31 | - Fix `react-native` support 32 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/WithAutoDocs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithAutoDocs from "./WithAutoDocs"; 4 | 5 | const meta = { 6 | component: WithAutoDocs, 7 | tags: ["autodocs"], 8 | parameters: { 9 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: "centered", 11 | deepControls: { 12 | enabled: true, 13 | }, 14 | }, 15 | args: { 16 | object: { 17 | booleanPropWithCustomDescription: true, 18 | requiredNumberProp: 5, 19 | }, 20 | }, 21 | argTypes: { 22 | "object.booleanPropWithCustomDescription": { 23 | description: "Custom description", 24 | }, 25 | "object.requiredNumberProp": { 26 | type: {required: true}, 27 | }, 28 | }, 29 | } satisfies Meta; 30 | 31 | export default meta; 32 | 33 | type Story = TypeWithDeepControls>; 34 | 35 | export const WithMergedArgTypes: Story = {}; 36 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithAutoDocs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithAutoDocs from "./WithAutoDocs"; 4 | 5 | const meta = { 6 | component: WithAutoDocs, 7 | tags: ["autodocs"], 8 | parameters: { 9 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: "centered", 11 | deepControls: { 12 | enabled: true, 13 | }, 14 | }, 15 | args: { 16 | object: { 17 | booleanPropWithCustomDescription: true, 18 | requiredNumberProp: 5, 19 | }, 20 | }, 21 | argTypes: { 22 | "object.booleanPropWithCustomDescription": { 23 | description: "Custom description", 24 | }, 25 | "object.requiredNumberProp": { 26 | type: {required: true}, 27 | }, 28 | }, 29 | } satisfies Meta; 30 | 31 | export default meta; 32 | 33 | type Story = TypeWithDeepControls>; 34 | 35 | export const WithMergedArgTypes: Story = {}; 36 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithAutoDocs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithAutoDocs from "./WithAutoDocs"; 4 | 5 | const meta = { 6 | component: WithAutoDocs, 7 | tags: ["autodocs"], 8 | parameters: { 9 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: "centered", 11 | deepControls: { 12 | enabled: true, 13 | }, 14 | }, 15 | args: { 16 | object: { 17 | booleanPropWithCustomDescription: true, 18 | requiredNumberProp: 5, 19 | }, 20 | }, 21 | argTypes: { 22 | "object.booleanPropWithCustomDescription": { 23 | description: "Custom description", 24 | }, 25 | "object.requiredNumberProp": { 26 | type: {required: true}, 27 | }, 28 | }, 29 | } satisfies Meta; 30 | 31 | export default meta; 32 | 33 | type Story = TypeWithDeepControls>; 34 | 35 | export const WithMergedArgTypes: Story = {}; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # see https://docs.netlify.com/configure-builds/file-based-configuration/#configuration-details 2 | # All paths configured in the netlify.toml should be absolute paths relative to the base directory, which is the root by default (/). 3 | # I assume this takes the folder with the .git folder as the base directory. 4 | [build] 5 | # see https://docs.netlify.com/configure-builds/file-based-configuration/#build-settings 6 | 7 | # directory where Netlify checks for dependency management files such as package.json or .nvmrc, installs dependencies, and runs your build command. 8 | # NOTE: needs to be root as this checks for changes in the base directory to know whether a deploy preview is needed, if it is only checking the 9 | # example package then addon changes wont get previewed 10 | base = "./" 11 | 12 | # the command to run to build your site 13 | command = "npm --prefix ./packages/example-v10-vite run build" 14 | 15 | # directory that contains the deploy-ready HTML files and assets generated by the build. The directory is relative to the base directory 16 | publish = "./packages/example-v10-vite/storybook-static" -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithAutoDocs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithAutoDocs from "./WithAutoDocs"; 4 | 5 | const meta = { 6 | component: WithAutoDocs, 7 | tags: ["autodocs"], 8 | parameters: { 9 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 10 | layout: "centered", 11 | deepControls: { 12 | enabled: true, 13 | }, 14 | }, 15 | args: { 16 | object: { 17 | booleanPropWithCustomDescription: true, 18 | requiredNumberProp: 5, 19 | }, 20 | }, 21 | argTypes: { 22 | "object.booleanPropWithCustomDescription": { 23 | description: "Custom description", 24 | }, 25 | "object.requiredNumberProp": { 26 | type: {required: true}, 27 | }, 28 | }, 29 | } satisfies Meta; 30 | 31 | export default meta; 32 | 33 | type Story = TypeWithDeepControls>; 34 | 35 | export const WithMergedArgTypes: Story = {}; 36 | -------------------------------------------------------------------------------- /packages/example-prod/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import {StorybookConfig} from "@storybook/react-vite"; 2 | import type {InlineConfig} from "vite"; 3 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 4 | 5 | // todo wait for react native 6 | const config: StorybookConfig = { 7 | stories: ["../../example-v9-generic/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 8 | addons: ["@storybook/addon-docs", "storybook-addon-deep-controls"], 9 | framework: { 10 | name: "@storybook/react-vite", 11 | options: {}, 12 | }, 13 | viteFinal: async (config) => { 14 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 15 | const [{mergeConfig}, react] = await Promise.all([ 16 | import("vite"), 17 | // to prevent "React is not defined" error without explicit react import 18 | import("@vitejs/plugin-react").then((m) => m.default), 19 | ]); 20 | 21 | return mergeConfig(config, { 22 | plugins: [react()], 23 | } satisfies InlineConfig); 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /packages/example-v10-vite/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import {StorybookConfig} from "@storybook/react-vite"; 2 | import type {InlineConfig} from "vite"; 3 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 4 | 5 | // todo wait for react native 6 | const config: StorybookConfig = { 7 | stories: ["../../example-v10-generic/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 8 | addons: ["@storybook/addon-docs", "storybook-addon-deep-controls"], 9 | framework: { 10 | name: "@storybook/react-vite", 11 | options: {}, 12 | }, 13 | viteFinal: async (config) => { 14 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 15 | const [{mergeConfig}, react] = await Promise.all([ 16 | import("vite"), 17 | // to prevent "React is not defined" error without explicit react import 18 | import("@vitejs/plugin-react").then((m) => m.default), 19 | ]); 20 | 21 | return mergeConfig(config, { 22 | plugins: [react()], 23 | } satisfies InlineConfig); 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /packages/example-v9-vite/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import {StorybookConfig} from "@storybook/react-vite"; 2 | import type {InlineConfig} from "vite"; 3 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 4 | 5 | // todo wait for react native 6 | const config: StorybookConfig = { 7 | stories: ["../../example-v9-generic/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 8 | addons: ["@storybook/addon-docs", "storybook-addon-deep-controls"], 9 | framework: { 10 | name: "@storybook/react-vite", 11 | options: {}, 12 | }, 13 | viteFinal: async (config) => { 14 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 15 | const [{mergeConfig}, react] = await Promise.all([ 16 | import("vite"), 17 | // to prevent "React is not defined" error without explicit react import 18 | import("@vitejs/plugin-react").then((m) => m.default), 19 | ]); 20 | 21 | return mergeConfig(config, { 22 | plugins: [react()], 23 | } satisfies InlineConfig); 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /.github/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/addon/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/example-v8-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v8-webpack", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "storybook": "echo 'Starting storybook' && npm run addon:build && storybook dev -p 6008 --no-open", 9 | "storybook:watch": "nodemon", 10 | "test": "npm --prefix ../example-v8-generic run playwright:v8-webpack -- test", 11 | "test:ui": "npm --prefix ../example-v8-generic run playwright:v8-webpack -- test --ui", 12 | "test:codegen": "npm --prefix ../example-v8-generic run playwright:v8-webpack -- codegen http://localhost:6008/", 13 | "addon:build": "echo 'Building addon...' && npm --prefix ../addon run build", 14 | "build": "npm run addon:build && storybook build" 15 | }, 16 | "devDependencies": { 17 | "@storybook/addon-essentials": "8.3.6", 18 | "@storybook/nextjs": "8.3.6", 19 | "@storybook/react": "8.3.6", 20 | "@storybook/types": "8.3.6", 21 | "next": "13.5.6", 22 | "nodemon": "3.1.10", 23 | "storybook": "8.3.6", 24 | "storybook-addon-deep-controls": "workspace:*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v7-webpack", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "storybook": "echo 'Starting storybook' && npm run addon:build && storybook dev -p 6007 --no-open", 9 | "storybook:watch": "nodemon", 10 | "addon:build": "echo 'Building addon...' && npm --prefix ../addon run build", 11 | "build": "npm run addon:build && storybook build", 12 | "playwright": "node ../../node_modules/playwright/cli.js", 13 | "test": "npm run playwright -- test", 14 | "test:ui": "npm run playwright -- test --ui", 15 | "test:codegen": "npm run playwright -- codegen http://localhost:6007/" 16 | }, 17 | "devDependencies": { 18 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 19 | "@storybook/addon-essentials": "7.6.20", 20 | "@storybook/nextjs": "7.6.20", 21 | "@storybook/react": "7.6.20", 22 | "@storybook/types": "7.6.20", 23 | "next": "13.5.6", 24 | "nodemon": "3.1.10", 25 | "storybook": "7.6.20", 26 | "storybook-addon-deep-controls": "workspace:*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-v8-vite/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import {StorybookConfig} from "@storybook/react-vite"; 2 | import type {InlineConfig} from "vite"; 3 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 4 | 5 | // todo wait for react native 6 | const config: StorybookConfig = { 7 | stories: ["../../example-v8-generic/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 8 | addons: [ 9 | "@storybook/addon-essentials", // to get controls and docs addons and make sure we are compatible with any other essential addon 10 | "storybook-addon-deep-controls", 11 | ], 12 | framework: { 13 | name: "@storybook/react-vite", 14 | options: {}, 15 | }, 16 | viteFinal: async (config) => { 17 | // NOTE: dont import vite at top level: https://github.com/storybookjs/storybook/issues/26291#issuecomment-1978193283 18 | const [{mergeConfig}, react] = await Promise.all([ 19 | import("vite"), 20 | import("@vitejs/plugin-react").then((m) => m.default), 21 | ]); 22 | 23 | return mergeConfig(config, { 24 | plugins: [react()], 25 | } satisfies InlineConfig); 26 | }, 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {createServer} from "http"; 2 | // @ts-expect-error - no declaration file, avoiding installing @types/lodash 3 | import cloneDeep from "lodash/cloneDeep"; 4 | import {STORYBOOK_PORT} from "./constants"; 5 | 6 | export function clone>(obj: T): T { 7 | return cloneDeep(obj); // maintains value as is, e.g. NaN, Infinity, etc. which JSON.stringify does not 8 | } 9 | 10 | function localHostPortIsInUse(port: number): Promise { 11 | return new Promise((resolve) => { 12 | const server = createServer(); 13 | server.on("error", () => { 14 | server.close(); 15 | resolve(true); 16 | }); 17 | server.on("listening", () => { 18 | server.close(); 19 | resolve(false); 20 | }); 21 | server.listen(port); 22 | }); 23 | } 24 | 25 | export async function assertStorybookIsRunning() { 26 | const isStorybookRunning = await localHostPortIsInUse(STORYBOOK_PORT); 27 | if (!isStorybookRunning) { 28 | throw new Error( 29 | `Storybook is not running (expected on localhost:${STORYBOOK_PORT}), please run 'npm run storybook' in a separate terminal`, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {createServer} from "http"; 2 | // @ts-expect-error - no declaration file, avoiding installing @types/lodash 3 | import cloneDeep from "lodash/cloneDeep"; 4 | import {STORYBOOK_PORT} from "./constants"; 5 | 6 | export function clone>(obj: T): T { 7 | return cloneDeep(obj); // maintains value as is, e.g. NaN, Infinity, etc. which JSON.stringify does not 8 | } 9 | 10 | function localHostPortIsInUse(port: number): Promise { 11 | return new Promise((resolve) => { 12 | const server = createServer(); 13 | server.on("error", () => { 14 | server.close(); 15 | resolve(true); 16 | }); 17 | server.on("listening", () => { 18 | server.close(); 19 | resolve(false); 20 | }); 21 | server.listen(port); 22 | }); 23 | } 24 | 25 | export async function assertStorybookIsRunning() { 26 | const isStorybookRunning = await localHostPortIsInUse(STORYBOOK_PORT); 27 | if (!isStorybookRunning) { 28 | throw new Error( 29 | `Storybook is not running (expected on localhost:${STORYBOOK_PORT}), please run 'npm run storybook' in a separate terminal`, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {createServer} from "http"; 2 | // @ts-expect-error - no declaration file, avoiding installing @types/lodash 3 | import cloneDeep from "lodash/cloneDeep"; 4 | import {STORYBOOK_PORT} from "./constants"; 5 | 6 | export function clone>(obj: T): T { 7 | return cloneDeep(obj); // maintains value as is, e.g. NaN, Infinity, etc. which JSON.stringify does not 8 | } 9 | 10 | function localHostPortIsInUse(port: number): Promise { 11 | return new Promise((resolve) => { 12 | const server = createServer(); 13 | server.on("error", () => { 14 | server.close(); 15 | resolve(true); 16 | }); 17 | server.on("listening", () => { 18 | server.close(); 19 | resolve(false); 20 | }); 21 | server.listen(port); 22 | }); 23 | } 24 | 25 | export async function assertStorybookIsRunning() { 26 | const isStorybookRunning = await localHostPortIsInUse(STORYBOOK_PORT); 27 | if (!isStorybookRunning) { 28 | throw new Error( 29 | `Storybook is not running (expected on localhost:${STORYBOOK_PORT}), please run 'npm run storybook' in a separate terminal`, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const TEST_TIMEOUT_MS = 120_000; 2 | 3 | type StorybookType = "vite"; 4 | 5 | type StorybookExampleConfig = { 6 | port: number; 7 | devCommand: string; 8 | }; 9 | 10 | const STORYBOOK_CONFIGS: Record = { 11 | vite: { 12 | port: 6100, 13 | devCommand: "npm run --prefix ../example-v9-vite storybook", 14 | }, 15 | }; 16 | 17 | // eslint-disable-next-line wrap-iife 18 | export const CONFIG = (function getStorybookConfig(): StorybookExampleConfig { 19 | const storybookType: StorybookType = getEnvironmentVariable("STORYBOOK_EXAMPLE_TYPE"); 20 | const config = STORYBOOK_CONFIGS[storybookType]; 21 | if (!config) { 22 | throw new Error(`Unknown storybook type ${storybookType}`); 23 | } 24 | 25 | // console.log(`Storybook config: ${JSON.stringify(config, null, 2)}`); 26 | return config; 27 | })(); 28 | 29 | export const STORYBOOK_PORT: number = CONFIG.port; 30 | 31 | function getEnvironmentVariable(name: "STORYBOOK_EXAMPLE_TYPE"): T { 32 | const value = process.env[name]; 33 | if (!value) { 34 | throw new Error(`Environment variable ${name} is not set`); 35 | } 36 | return value as T; 37 | } 38 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const TEST_TIMEOUT_MS = 120_000; 2 | 3 | type StorybookType = "vite"; 4 | 5 | type StorybookExampleConfig = { 6 | port: number; 7 | devCommand: string; 8 | }; 9 | 10 | const STORYBOOK_CONFIGS: Record = { 11 | vite: { 12 | port: 6200, 13 | devCommand: "npm run --prefix ../example-v10-vite storybook", 14 | }, 15 | }; 16 | 17 | // eslint-disable-next-line wrap-iife 18 | export const CONFIG = (function getStorybookConfig(): StorybookExampleConfig { 19 | const storybookType: StorybookType = getEnvironmentVariable("STORYBOOK_EXAMPLE_TYPE"); 20 | const config = STORYBOOK_CONFIGS[storybookType]; 21 | if (!config) { 22 | throw new Error(`Unknown storybook type ${storybookType}`); 23 | } 24 | 25 | // console.log(`Storybook config: ${JSON.stringify(config, null, 2)}`); 26 | return config; 27 | })(); 28 | 29 | export const STORYBOOK_PORT: number = CONFIG.port; 30 | 31 | function getEnvironmentVariable(name: "STORYBOOK_EXAMPLE_TYPE"): T { 32 | const value = process.env[name]; 33 | if (!value) { 34 | throw new Error(`Environment variable ${name} is not set`); 35 | } 36 | return value as T; 37 | } 38 | -------------------------------------------------------------------------------- /scripts/sync-github-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is to avoid using a symlink to the LICENSE.md and README.md files in the .github folder. 3 | * 4 | * This is because symlinks seem to cause errors when cloning the repo, e.g.: 5 | * - https://github.com/eliasm307/storybook-addon-deep-controls/issues/29 6 | * - https://answers.netlify.com/t/error-unable-to-create-symlink-github-readme-md-file-name-too-long/106757 7 | */ 8 | 9 | const fs = require("fs"); 10 | const path = require("path"); 11 | 12 | const filesToSync = ["LICENSE.md", "README.md"]; 13 | const sourceDir = path.resolve(__dirname, "../packages/addon"); 14 | const targetDir = path.resolve(__dirname, "..", ".github"); 15 | 16 | filesToSync.forEach((file) => { 17 | const sourcePath = path.join(sourceDir, file); 18 | const targetPath = path.join(targetDir, file); 19 | 20 | fs.readFile(sourcePath, "utf8", (err, data) => { 21 | if (err) { 22 | throw new Error(`Error reading ${file} from source directory: ${err.message}`); 23 | } 24 | 25 | fs.writeFile(targetPath, data, "utf8", (err) => { 26 | if (err) { 27 | throw new Error(`Error writing ${file} to target directory: ${err.message}`); 28 | } 29 | 30 | console.log(`${file} has been successfully synced to the .github folder.`); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/Basic.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Basic from "./Basic"; 4 | 5 | const meta: TypeWithDeepControls> = { 6 | component: Basic, 7 | parameters: { 8 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 9 | layout: "centered", 10 | deepControls: { 11 | enabled: true, 12 | }, 13 | }, 14 | }; 15 | 16 | export default meta; 17 | 18 | type Story = TypeWithDeepControls>; 19 | 20 | export const Disabled: Story = { 21 | args: { 22 | someObject: { 23 | anyString: "anyString", 24 | enumString: "value2", 25 | number: 42, 26 | boolean: true, 27 | }, 28 | }, 29 | parameters: { 30 | deepControls: { 31 | enabled: false, 32 | }, 33 | }, 34 | }; 35 | 36 | export const Enabled: Story = { 37 | args: { 38 | someObject: { 39 | anyString: "anyString", 40 | enumString: "value2", 41 | number: 42, 42 | boolean: true, 43 | }, 44 | }, 45 | argTypes: { 46 | "someObject.enumString": { 47 | control: "radio", 48 | options: ["value1", "value2", "value3"], 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/Basic.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Basic from "./Basic"; 4 | 5 | const meta: TypeWithDeepControls> = { 6 | component: Basic, 7 | parameters: { 8 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 9 | layout: "centered", 10 | deepControls: { 11 | enabled: true, 12 | }, 13 | }, 14 | }; 15 | 16 | export default meta; 17 | 18 | type Story = TypeWithDeepControls>; 19 | 20 | export const Disabled: Story = { 21 | args: { 22 | someObject: { 23 | anyString: "anyString", 24 | enumString: "value2", 25 | number: 42, 26 | boolean: true, 27 | }, 28 | }, 29 | parameters: { 30 | deepControls: { 31 | enabled: false, 32 | }, 33 | }, 34 | }; 35 | 36 | export const Enabled: Story = { 37 | args: { 38 | someObject: { 39 | anyString: "anyString", 40 | enumString: "value2", 41 | number: 42, 42 | boolean: true, 43 | }, 44 | }, 45 | argTypes: { 46 | "someObject.enumString": { 47 | control: "radio", 48 | options: ["value1", "value2", "value3"], 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/Basic.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Basic from "./Basic"; 4 | 5 | const meta: TypeWithDeepControls> = { 6 | component: Basic, 7 | parameters: { 8 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 9 | layout: "centered", 10 | deepControls: { 11 | enabled: true, 12 | }, 13 | }, 14 | }; 15 | 16 | export default meta; 17 | 18 | type Story = TypeWithDeepControls>; 19 | 20 | export const Disabled: Story = { 21 | args: { 22 | someObject: { 23 | anyString: "anyString", 24 | enumString: "value2", 25 | number: 42, 26 | boolean: true, 27 | }, 28 | }, 29 | parameters: { 30 | deepControls: { 31 | enabled: false, 32 | }, 33 | }, 34 | }; 35 | 36 | export const Enabled: Story = { 37 | args: { 38 | someObject: { 39 | anyString: "anyString", 40 | enumString: "value2", 41 | number: 42, 42 | boolean: true, 43 | }, 44 | }, 45 | argTypes: { 46 | "someObject.enumString": { 47 | control: "radio", 48 | options: ["value1", "value2", "value3"], 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Addon 2 | 3 | Thank you for your interest in contributing! This document provides some tips for contributing to the addon. 4 | 5 | ## Addon Code Location 6 | 7 | The addon code is in `packages/addon`. 8 | 9 | ## Testing 10 | 11 | Unit tests are in `packages/addon/test` and they are not included when publishing. 12 | 13 | End-to-end tests are in `packages/example-*` packages for different Storybook versions and frameworks. Generally Vite is the preferred framework for examples, but others can be added if there is a specific need e.g. recreating an issue. 14 | 15 | For end-to-end tests, in some cases there is a `packages/example-*-generic` package which represents generic tests for a specific version of Storybook. These tests are not framework specific and can be used to run tests against different frameworks. 16 | 17 | ## Development Examples 18 | 19 | For running the local addon with Storybook use the example apps in the `packages/` directory with the `example-` prefix and a storybook version and framework. 20 | 21 | The example apps have scripts to run Storybook with the local addon (ie `yarn storybook`) and also to run Storybook with the addon in watch mode (ie `yarn storybook:watch`). 22 | 23 | ## Production Example 24 | 25 | For testing end-to-end integration of the published addon with Storybook use the app in the `packages/example-prod` directory. 26 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const TEST_TIMEOUT_MS = 120_000; 2 | 3 | type StorybookType = "v8-vite" | "v8-webpack"; 4 | 5 | type StorybookExampleConfig = { 6 | port: number; 7 | devCommand: string; 8 | }; 9 | 10 | const STORYBOOK_CONFIGS: Record<"v8-vite" | "v8-webpack", StorybookExampleConfig> = { 11 | "v8-webpack": { 12 | port: 6008, 13 | devCommand: "npm run --prefix ../example-v8-webpack storybook", 14 | }, 15 | "v8-vite": { 16 | port: 6018, 17 | devCommand: "npm run --prefix ../example-v8-vite storybook", 18 | }, 19 | }; 20 | 21 | // eslint-disable-next-line wrap-iife 22 | export const CONFIG = (function getStorybookConfig(): StorybookExampleConfig { 23 | const storybookType: StorybookType = getEnvironmentVariable("STORYBOOK_EXAMPLE_TYPE"); 24 | const config = STORYBOOK_CONFIGS[storybookType]; 25 | if (!config) { 26 | throw new Error(`Unknown storybook type ${storybookType}`); 27 | } 28 | 29 | // console.log(`Storybook config: ${JSON.stringify(config, null, 2)}`); 30 | return config; 31 | })(); 32 | 33 | export const STORYBOOK_PORT: number = CONFIG.port; 34 | 35 | function getEnvironmentVariable(name: "STORYBOOK_EXAMPLE_TYPE"): T { 36 | const value = process.env[name]; 37 | if (!value) { 38 | throw new Error(`Environment variable ${name} is not set`); 39 | } 40 | return value as T; 41 | } 42 | -------------------------------------------------------------------------------- /packages/example-v8-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v8-vite", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "storybook": "echo 'Starting storybook' && npm run addon:build && cross-env VITE_CJS_TRACE=true storybook dev -p 6018 --no-open", 9 | "storybook:watch": "cross-env VITE_CJS_TRACE=true nodemon", 10 | "test": "npm --prefix ../example-v8-generic run playwright:v8-vite -- test", 11 | "test:ui": "npm --prefix ../example-v8-generic run playwright:v8-vite -- test --ui", 12 | "test:codegen": "npm --prefix ../example-v8-generic run playwright:v8-vite -- codegen http://localhost:6018/", 13 | "addon:build": "echo 'Building addon...' && npm --prefix ../addon run build", 14 | "build": "npm run addon:build && storybook build" 15 | }, 16 | "devDependencies": { 17 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 18 | "@storybook/addon-essentials": "8.3.6", 19 | "@storybook/builder-vite": "8.3.6", 20 | "@storybook/react": "8.3.6", 21 | "@storybook/react-vite": "8.3.6", 22 | "@storybook/types": "8.3.6", 23 | "@vitejs/plugin-react": "4.5.2", 24 | "cross-env": "7.0.3", 25 | "nodemon": "3.1.10", 26 | "storybook": "8.3.6", 27 | "storybook-addon-deep-controls": "workspace:*", 28 | "vite": "6.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-v9-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v9-vite", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "storybook": "echo 'Starting storybook' && npm run addon:build && cross-env VITE_CJS_TRACE=true storybook dev -p 6100 --no-open", 9 | "storybook:watch": "cross-env VITE_CJS_TRACE=true nodemon", 10 | "test": "npm --prefix ../example-v9-generic run playwright:vite -- test", 11 | "test:ui": "npm --prefix ../example-v9-generic run playwright:vite -- test --ui", 12 | "test:codegen": "npm --prefix ../example-v9-generic run playwright:vite -- codegen http://localhost:6100/", 13 | "addon:build": "echo 'Building addon...' && npm --prefix ../addon run build", 14 | "build": "npm run addon:build && storybook build", 15 | "deploy": "npm run build && netlify deploy --prod" 16 | }, 17 | "devDependencies": { 18 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 19 | "@storybook/addon-docs": "9.0.12", 20 | "@storybook/builder-vite": "9.0.12", 21 | "@storybook/react": "9.0.12", 22 | "@storybook/react-vite": "9.0.12", 23 | "@vitejs/plugin-react": "4.5.2", 24 | "cross-env": "7.0.3", 25 | "nodemon": "3.1.10", 26 | "storybook": "9.0.12", 27 | "storybook-addon-deep-controls": "workspace:*", 28 | "vite": "6.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-v10-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls-example-v10-vite", 3 | "private": true, 4 | "installConfig": { 5 | "hoistingLimits": "workspaces" 6 | }, 7 | "scripts": { 8 | "storybook": "echo 'Starting storybook' && npm run addon:build && cross-env VITE_CJS_TRACE=true storybook dev -p 6200 --no-open", 9 | "storybook:watch": "cross-env VITE_CJS_TRACE=true nodemon", 10 | "test": "npm --prefix ../example-v10-generic run playwright:vite -- test", 11 | "test:ui": "npm --prefix ../example-v10-generic run playwright:vite -- test --ui", 12 | "test:codegen": "npm --prefix ../example-v10-generic run playwright:vite -- codegen http://localhost:6200/", 13 | "addon:build": "echo 'Building addon...' && npm --prefix ../addon run build", 14 | "build": "npm run addon:build && storybook build", 15 | "deploy": "npm run build && netlify deploy --prod" 16 | }, 17 | "devDependencies": { 18 | "@storybook-addon-deep-controls/common-internal": "workspace:*", 19 | "@storybook/addon-docs": "10.0.2", 20 | "@storybook/builder-vite": "10.0.2", 21 | "@storybook/react": "10.0.2", 22 | "@storybook/react-vite": "10.0.2", 23 | "@vitejs/plugin-react": "4.5.2", 24 | "cross-env": "7.0.3", 25 | "nodemon": "3.1.10", 26 | "storybook": "10.0.2", 27 | "storybook-addon-deep-controls": "workspace:*", 28 | "vite": "6.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/WithTypedProps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithTypedProps from "./WithTypedProps"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: WithTypedProps, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: {enabled: true}, 13 | }, 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const DefaultEnabled: Story = {}; 20 | 21 | export const DefaultDisabled: Story = { 22 | parameters: { 23 | deepControls: { 24 | enabled: false, 25 | }, 26 | }, 27 | }; 28 | 29 | export const WithArgs: Story = { 30 | args: { 31 | someObject: { 32 | anyString: "anyString", 33 | enumString: "value2", 34 | }, 35 | someArray: ["string1", "string2"], 36 | // NOTE: we inherit the "someString" control from docs without an arg value 37 | }, 38 | }; 39 | 40 | export const WithCustomControls: TypeWithDeepControls = { 41 | args: { 42 | someObject: { 43 | anyString: "anyString", 44 | enumString: "value2", 45 | }, 46 | someArray: ["string1", "string2"], 47 | }, 48 | argTypes: { 49 | "someObject.enumString": { 50 | control: "radio", 51 | options: ["value1", "value2", "value3"], 52 | }, 53 | someString: { 54 | control: "radio", 55 | options: ["string1", "string2", "string3"], 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithTypedProps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithTypedProps from "./WithTypedProps"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: WithTypedProps, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: {enabled: true}, 13 | }, 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const DefaultEnabled: Story = {}; 20 | 21 | export const DefaultDisabled: Story = { 22 | parameters: { 23 | deepControls: { 24 | enabled: false, 25 | }, 26 | }, 27 | }; 28 | 29 | export const WithArgs: Story = { 30 | args: { 31 | someObject: { 32 | anyString: "anyString", 33 | enumString: "value2", 34 | }, 35 | someArray: ["string1", "string2"], 36 | // NOTE: we inherit the "someString" control from docs without an arg value 37 | }, 38 | }; 39 | 40 | export const WithCustomControls: TypeWithDeepControls = { 41 | args: { 42 | someObject: { 43 | anyString: "anyString", 44 | enumString: "value2", 45 | }, 46 | someArray: ["string1", "string2"], 47 | }, 48 | argTypes: { 49 | "someObject.enumString": { 50 | control: "radio", 51 | options: ["value1", "value2", "value3"], 52 | }, 53 | someString: { 54 | control: "radio", 55 | options: ["string1", "string2", "string3"], 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithTypedProps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithTypedProps from "./WithTypedProps"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: WithTypedProps, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: {enabled: true}, 13 | }, 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const DefaultEnabled: Story = {}; 20 | 21 | export const DefaultDisabled: Story = { 22 | parameters: { 23 | deepControls: { 24 | enabled: false, 25 | }, 26 | }, 27 | }; 28 | 29 | export const WithArgs: Story = { 30 | args: { 31 | someObject: { 32 | anyString: "anyString", 33 | enumString: "value2", 34 | }, 35 | someArray: ["string1", "string2"], 36 | // NOTE: we inherit the "someString" control from docs without an arg value 37 | }, 38 | }; 39 | 40 | export const WithCustomControls: TypeWithDeepControls = { 41 | args: { 42 | someObject: { 43 | anyString: "anyString", 44 | enumString: "value2", 45 | }, 46 | someArray: ["string1", "string2"], 47 | }, 48 | argTypes: { 49 | "someObject.enumString": { 50 | control: "radio", 51 | options: ["value1", "value2", "value3"], 52 | }, 53 | someString: { 54 | control: "radio", 55 | options: ["string1", "string2", "string3"], 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithTypedProps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithTypedProps from "./WithTypedProps"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: WithTypedProps, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: {enabled: true}, 13 | }, 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const DefaultEnabled: Story = {}; 20 | 21 | export const DefaultDisabled: Story = { 22 | parameters: { 23 | deepControls: { 24 | enabled: false, 25 | }, 26 | }, 27 | }; 28 | 29 | export const WithArgs: Story = { 30 | args: { 31 | someObject: { 32 | anyString: "anyString", 33 | enumString: "value2", 34 | }, 35 | someArray: ["string1", "string2"], 36 | // NOTE: we inherit the "someString" control from docs without an arg value 37 | }, 38 | }; 39 | 40 | export const WithCustomControls: TypeWithDeepControls = { 41 | args: { 42 | someObject: { 43 | anyString: "anyString", 44 | enumString: "value2", 45 | }, 46 | someArray: ["string1", "string2"], 47 | }, 48 | argTypes: { 49 | "someObject.enumString": { 50 | control: "radio", 51 | options: ["value1", "value2", "value3"], 52 | }, 53 | someString: { 54 | control: "radio", 55 | options: ["string1", "string2", "string3"], 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from "@playwright/test"; 2 | import {STORYBOOK_V7_PORT} from "./src/tests/utils/constants"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./src/tests", 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: [["html", {open: "never"}]], 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: "on-first-retry", 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | // we just need to test one browser, we are testing storybook functionality not its browser compatibility 37 | { 38 | name: "chromium", 39 | use: {...devices["Desktop Chrome"]}, 40 | }, 41 | ], 42 | 43 | /** @see https://playwright.dev/docs/api/class-testconfig#test-config-web-server */ 44 | webServer: { 45 | command: "npm run storybook", 46 | url: `http://localhost:${STORYBOOK_V7_PORT}/`, 47 | reuseExistingServer: !process.env.CI, 48 | timeout: 120 * 1000, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/WithTypedProps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import WithTypedProps from "./WithTypedProps"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: WithTypedProps, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | export const DefaultEnabled: Story = {}; 22 | 23 | export const DefaultDisabled: Story = { 24 | parameters: { 25 | deepControls: { 26 | enabled: false, 27 | }, 28 | }, 29 | }; 30 | 31 | export const WithArgs: Story = { 32 | args: { 33 | someObject: { 34 | anyString: "anyString", 35 | enumString: "enumString", 36 | }, 37 | someArray: ["string1", "string2"], 38 | // NOTE: we inherit the "someString" control from docs without an arg value 39 | }, 40 | }; 41 | 42 | export const WithCustomControls: TypeWithDeepControls = { 43 | args: { 44 | someObject: { 45 | anyString: "anyString", 46 | enumString: "enumString", 47 | }, 48 | someArray: ["string1", "string2"], 49 | }, 50 | argTypes: { 51 | "someObject.enumString": { 52 | control: "radio", 53 | options: ["value1", "value2", "value3"], 54 | }, 55 | someString: { 56 | control: "radio", 57 | options: ["string1", "string2", "string3"], 58 | }, 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /packages/example-v10-generic/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from "@playwright/test"; 2 | import {CONFIG, STORYBOOK_PORT, TEST_TIMEOUT_MS} from "./src/utils/constants.js"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./src/stories", 15 | globalSetup: require.resolve("./global-setup"), 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 1, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: [["html", {open: "never"}]], 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | // baseURL: 'http://127.0.0.1:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: "on-first-retry", 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | // we just need to test one browser, we are testing storybook functionality not its browser compatibility 38 | { 39 | name: "chromium", 40 | use: {...devices["Desktop Chrome"]}, 41 | }, 42 | ], 43 | 44 | quiet: false, 45 | 46 | /** @see https://playwright.dev/docs/api/class-testconfig#test-config-web-server */ 47 | webServer: { 48 | command: CONFIG.devCommand, 49 | url: `http://localhost:${STORYBOOK_PORT}/`, 50 | reuseExistingServer: !process.env.CI, 51 | timeout: TEST_TIMEOUT_MS, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /packages/example-v8-generic/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from "@playwright/test"; 2 | import {CONFIG, STORYBOOK_PORT, TEST_TIMEOUT_MS} from "./src/utils/constants.js"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./src/stories", 15 | globalSetup: require.resolve("./global-setup"), 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 1, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: [["html", {open: "never"}]], 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | // baseURL: 'http://127.0.0.1:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: "on-first-retry", 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | // we just need to test one browser, we are testing storybook functionality not its browser compatibility 38 | { 39 | name: "chromium", 40 | use: {...devices["Desktop Chrome"]}, 41 | }, 42 | ], 43 | 44 | quiet: false, 45 | 46 | /** @see https://playwright.dev/docs/api/class-testconfig#test-config-web-server */ 47 | webServer: { 48 | command: CONFIG.devCommand, 49 | url: `http://localhost:${STORYBOOK_PORT}/`, 50 | reuseExistingServer: !process.env.CI, 51 | timeout: TEST_TIMEOUT_MS, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /packages/example-v9-generic/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from "@playwright/test"; 2 | import {CONFIG, STORYBOOK_PORT, TEST_TIMEOUT_MS} from "./src/utils/constants.js"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./src/stories", 15 | globalSetup: require.resolve("./global-setup"), 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 1, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: [["html", {open: "never"}]], 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | // baseURL: 'http://127.0.0.1:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: "on-first-retry", 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | // we just need to test one browser, we are testing storybook functionality not its browser compatibility 38 | { 39 | name: "chromium", 40 | use: {...devices["Desktop Chrome"]}, 41 | }, 42 | ], 43 | 44 | quiet: false, 45 | 46 | /** @see https://playwright.dev/docs/api/class-testconfig#test-config-web-server */ 47 | webServer: { 48 | command: CONFIG.devCommand, 49 | url: `http://localhost:${STORYBOOK_PORT}/`, 50 | reuseExistingServer: !process.env.CI, 51 | timeout: TEST_TIMEOUT_MS, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/tests/types.ts: -------------------------------------------------------------------------------- 1 | import type {Locator} from "playwright/test"; 2 | 3 | export type GeneralControlRowExpectation = { 4 | isRequired?: boolean; 5 | descriptionLines?: string[]; 6 | }; 7 | 8 | /** 9 | * Providing primitive values infers a basic expectation 10 | */ 11 | export type ControlExpectation = 12 | | StringControlExpectation 13 | | NumberControlExpectation 14 | | BooleanControlExpectation 15 | | SetValueButtonControlExpectation 16 | | RadioControlExpectation 17 | | ColorControlExpectation 18 | | JsonControlExpectation 19 | | string 20 | | number 21 | | boolean; 22 | 23 | export type BooleanControlExpectation = GeneralControlRowExpectation & { 24 | type: "boolean"; 25 | value: boolean; 26 | }; 27 | 28 | export type StringControlExpectation = GeneralControlRowExpectation & { 29 | type: "string"; 30 | value: string; 31 | }; 32 | 33 | export type NumberControlExpectation = GeneralControlRowExpectation & { 34 | type: "number"; 35 | value: number; 36 | }; 37 | 38 | export type SetValueButtonControlExpectation = GeneralControlRowExpectation & { 39 | type: "set-value-button"; 40 | valueType: "string" | "object"; 41 | }; 42 | 43 | export type RadioControlExpectation = GeneralControlRowExpectation & { 44 | type: "radio"; 45 | options: string[]; 46 | value: string | null; 47 | }; 48 | 49 | export type ColorControlExpectation = GeneralControlRowExpectation & { 50 | type: "color"; 51 | value: string; 52 | }; 53 | 54 | export type JsonControlExpectation = GeneralControlRowExpectation & { 55 | /** 56 | * @remark control cant be parsed as its string value is not valid JSON 57 | */ 58 | type: "json"; 59 | /** 60 | * This is the displayed text in the control, it might not be valid JSON and some sections might be collapsed 61 | * so we do a general assertion of the state of the control 62 | */ 63 | valueText: string; 64 | }; 65 | 66 | export type ControlDetails = { 67 | name: string; 68 | inputLocator: Locator; 69 | rowLocator: Locator; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/tests/types.ts: -------------------------------------------------------------------------------- 1 | import type {Locator} from "playwright/test"; 2 | 3 | export type GeneralControlRowExpectation = { 4 | isRequired?: boolean; 5 | descriptionLines?: string[]; 6 | }; 7 | 8 | /** 9 | * Providing primitive values infers a basic expectation 10 | */ 11 | export type ControlExpectation = 12 | | StringControlExpectation 13 | | NumberControlExpectation 14 | | BooleanControlExpectation 15 | | SetValueButtonControlExpectation 16 | | RadioControlExpectation 17 | | ColorControlExpectation 18 | | JsonControlExpectation 19 | | string 20 | | number 21 | | boolean; 22 | 23 | export type BooleanControlExpectation = GeneralControlRowExpectation & { 24 | type: "boolean"; 25 | value: boolean; 26 | }; 27 | 28 | export type StringControlExpectation = GeneralControlRowExpectation & { 29 | type: "string"; 30 | value: string; 31 | }; 32 | 33 | export type NumberControlExpectation = GeneralControlRowExpectation & { 34 | type: "number"; 35 | value: number; 36 | }; 37 | 38 | export type SetValueButtonControlExpectation = GeneralControlRowExpectation & { 39 | type: "set-value-button"; 40 | valueType: "string" | "object"; 41 | }; 42 | 43 | export type RadioControlExpectation = GeneralControlRowExpectation & { 44 | type: "radio"; 45 | options: string[]; 46 | value: string | null; 47 | }; 48 | 49 | export type ColorControlExpectation = GeneralControlRowExpectation & { 50 | type: "color"; 51 | value: string; 52 | }; 53 | 54 | export type JsonControlExpectation = GeneralControlRowExpectation & { 55 | /** 56 | * @remark control cant be parsed as its string value is not valid JSON 57 | */ 58 | type: "json"; 59 | /** 60 | * This is the displayed text in the control, it might not be valid JSON and some sections might be collapsed 61 | * so we do a general assertion of the state of the control 62 | */ 63 | valueText: string; 64 | }; 65 | 66 | export type ControlDetails = { 67 | name: string; 68 | inputLocator: Locator; 69 | rowLocator: Locator; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/tests/types.ts: -------------------------------------------------------------------------------- 1 | import type {Locator} from "playwright/test"; 2 | 3 | export type GeneralControlRowExpectation = { 4 | isRequired?: boolean; 5 | descriptionLines?: string[]; 6 | }; 7 | 8 | /** 9 | * Providing primitive values infers a basic expectation 10 | */ 11 | export type ControlExpectation = 12 | | StringControlExpectation 13 | | NumberControlExpectation 14 | | BooleanControlExpectation 15 | | SetValueButtonControlExpectation 16 | | RadioControlExpectation 17 | | ColorControlExpectation 18 | | JsonControlExpectation 19 | | string 20 | | number 21 | | boolean; 22 | 23 | export type BooleanControlExpectation = GeneralControlRowExpectation & { 24 | type: "boolean"; 25 | value: boolean; 26 | }; 27 | 28 | export type StringControlExpectation = GeneralControlRowExpectation & { 29 | type: "string"; 30 | value: string; 31 | }; 32 | 33 | export type NumberControlExpectation = GeneralControlRowExpectation & { 34 | type: "number"; 35 | value: number; 36 | }; 37 | 38 | export type SetValueButtonControlExpectation = GeneralControlRowExpectation & { 39 | type: "set-value-button"; 40 | valueType: "string" | "object"; 41 | }; 42 | 43 | export type RadioControlExpectation = GeneralControlRowExpectation & { 44 | type: "radio"; 45 | options: string[]; 46 | value: string | null; 47 | }; 48 | 49 | export type ColorControlExpectation = GeneralControlRowExpectation & { 50 | type: "color"; 51 | value: string; 52 | }; 53 | 54 | export type JsonControlExpectation = GeneralControlRowExpectation & { 55 | /** 56 | * @remark control cant be parsed as its string value is not valid JSON 57 | */ 58 | type: "json"; 59 | /** 60 | * This is the displayed text in the control, it might not be valid JSON and some sections might be collapsed 61 | * so we do a general assertion of the state of the control 62 | */ 63 | valueText: string; 64 | }; 65 | 66 | export type ControlDetails = { 67 | name: string; 68 | inputLocator: Locator; 69 | rowLocator: Locator; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithAutoDocs.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows story with merged arg types correctly", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withautodocs--with-merged-arg-types", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.assert.controlsMatch({ 22 | "object.requiredNumberProp": { 23 | type: "number", 24 | value: 5, 25 | isRequired: true, 26 | }, 27 | "object.booleanPropWithCustomDescription": true, 28 | }); 29 | await storyPage.assert.actualConfigMatches({ 30 | object: { 31 | booleanPropWithCustomDescription: true, 32 | requiredNumberProp: 5, 33 | }, 34 | }); 35 | }); 36 | 37 | test("shows docs page with merged arg types correctly", async ({page}) => { 38 | const storybookPage = new AppObject(page); 39 | await storybookPage.action.openStoriesTreeItemById("docs", "stories-withautodocs--docs"); 40 | 41 | const docsPage = storybookPage.activeDocsPage; 42 | await docsPage.assert.actualConfigMatches({ 43 | object: { 44 | booleanPropWithCustomDescription: true, 45 | requiredNumberProp: 5, 46 | }, 47 | }); 48 | await docsPage.assert.controlsMatch({ 49 | "object.booleanPropWithCustomDescription": { 50 | type: "boolean", 51 | value: true, 52 | descriptionLines: ["Custom description", "boolean"], 53 | }, 54 | "object.requiredNumberProp": { 55 | type: "number", 56 | value: 5, 57 | isRequired: true, 58 | descriptionLines: ["number"], 59 | }, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithAutoDocs.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows story with merged arg types correctly", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withautodocs--with-merged-arg-types", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.assert.controlsMatch({ 22 | "object.requiredNumberProp": { 23 | type: "number", 24 | value: 5, 25 | isRequired: true, 26 | }, 27 | "object.booleanPropWithCustomDescription": true, 28 | }); 29 | await storyPage.assert.actualConfigMatches({ 30 | object: { 31 | booleanPropWithCustomDescription: true, 32 | requiredNumberProp: 5, 33 | }, 34 | }); 35 | }); 36 | 37 | test("shows docs page with merged arg types correctly", async ({page}) => { 38 | const storybookPage = new AppObject(page); 39 | await storybookPage.action.openStoriesTreeItemById("docs", "stories-withautodocs--docs"); 40 | 41 | const docsPage = storybookPage.activeDocsPage; 42 | await docsPage.assert.actualConfigMatches({ 43 | object: { 44 | booleanPropWithCustomDescription: true, 45 | requiredNumberProp: 5, 46 | }, 47 | }); 48 | await docsPage.assert.controlsMatch({ 49 | "object.booleanPropWithCustomDescription": { 50 | type: "boolean", 51 | value: true, 52 | descriptionLines: ["Custom description", "boolean"], 53 | }, 54 | "object.requiredNumberProp": { 55 | type: "number", 56 | value: 5, 57 | isRequired: true, 58 | descriptionLines: ["number"], 59 | }, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithAutoDocs.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows story with merged arg types correctly", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withautodocs--with-merged-arg-types", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.assert.controlsMatch({ 22 | "object.requiredNumberProp": { 23 | type: "number", 24 | value: 5, 25 | isRequired: true, 26 | }, 27 | "object.booleanPropWithCustomDescription": true, 28 | }); 29 | await storyPage.assert.actualConfigMatches({ 30 | object: { 31 | booleanPropWithCustomDescription: true, 32 | requiredNumberProp: 5, 33 | }, 34 | }); 35 | }); 36 | 37 | test("shows docs page with merged arg types correctly", async ({page}) => { 38 | const storybookPage = new AppObject(page); 39 | await storybookPage.action.openStoriesTreeItemById("docs", "stories-withautodocs--docs"); 40 | 41 | const docsPage = storybookPage.activeDocsPage; 42 | await docsPage.assert.actualConfigMatches({ 43 | object: { 44 | booleanPropWithCustomDescription: true, 45 | requiredNumberProp: 5, 46 | }, 47 | }); 48 | await docsPage.assert.controlsMatch({ 49 | "object.booleanPropWithCustomDescription": { 50 | type: "boolean", 51 | value: true, 52 | descriptionLines: ["Custom description", "boolean"], 53 | }, 54 | "object.requiredNumberProp": { 55 | type: "number", 56 | value: 5, 57 | isRequired: true, 58 | descriptionLines: ["number"], 59 | }, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/addon/src/preview.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ProjectAnnotations, 3 | Renderer, 4 | StrictArgTypes, 5 | } from "storybook/internal/types" with {"resolution-mode": "import"}; 6 | import {createFlattenedArgTypes, createFlattenedArgs, expandObject} from "./utils/story"; 7 | 8 | // todo test it does not do anything custom if deepControls is not enabled 9 | const preview: ProjectAnnotations = { 10 | argsEnhancers: [ 11 | /** 12 | * If enabled, adds initial args to fit the flattened controls 13 | * 14 | * @note This only gets called when the story is rendered (ie not when controls change etc) 15 | * 16 | * @note Might be called multiple times during render for the same story 17 | */ 18 | (context) => { 19 | if (!context.parameters.deepControls?.enabled) { 20 | return context.initialArgs; 21 | } 22 | 23 | return createFlattenedArgs(context); 24 | }, 25 | ], 26 | 27 | argTypesEnhancers: [ 28 | /** 29 | * If enabled, replaces controls with flattened controls based on the initial args 30 | * and these will be what the user interacts with, ie the flat args become the source of truth 31 | * 32 | * @note Storybook still adds in the un-flattened args but these should be ignored 33 | * 34 | * @note This only gets called when the story is rendered (ie not when controls change etc) 35 | * 36 | * @note Might be called multiple times during render for the same story 37 | */ 38 | (context) => { 39 | if (!context.parameters.deepControls?.enabled) { 40 | return context.argTypes; 41 | } 42 | 43 | return createFlattenedArgTypes(context) as StrictArgTypes; 44 | }, 45 | ], 46 | 47 | decorators: [ 48 | /** 49 | * If enabled, un-flattens the args from controls to the original format 50 | * before passing them to the story component 51 | */ 52 | (storyFn, context) => { 53 | if (!context.parameters.deepControls?.enabled) { 54 | return storyFn(context); 55 | } 56 | 57 | return storyFn({ 58 | ...context, 59 | args: expandObject(context.args), 60 | initialArgs: expandObject(context.initialArgs), 61 | }); 62 | }, 63 | ], 64 | }; 65 | 66 | export default preview; 67 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/tests/objects/DocsPageObject.ts: -------------------------------------------------------------------------------- 1 | import {expect, type Page} from "playwright/test"; 2 | import type {ControlExpectation} from "../types"; 3 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 4 | 5 | class Waits { 6 | constructor(private object: DocsPageObject) {} 7 | 8 | async previewIframeLoaded() { 9 | await this.object.previewIframeLocator.owner().isVisible(); 10 | await this.object.previewLoader.isHidden(); 11 | 12 | // wait for iframe to have attribute 13 | await this.object.page.waitForSelector( 14 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 15 | {state: "visible"}, 16 | ); 17 | 18 | // make sure docs loaded 19 | await this.object.docsRoot.waitFor({state: "visible"}); 20 | } 21 | } 22 | 23 | class Assertions { 24 | constructor(private object: DocsPageObject) {} 25 | 26 | async actualConfigMatches(expectedConfig: Record) { 27 | const actualConfigText = await this.object.docsRoot // story is rendered but not visible, so here we are specifying the docs root 28 | .locator("#actual-config-json") 29 | .innerText(); 30 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 31 | } 32 | 33 | async controlsMatch(expectedControlsMap: Record) { 34 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 35 | } 36 | } 37 | 38 | class Actions { 39 | constructor(private object: DocsPageObject) {} 40 | } 41 | 42 | /** 43 | * Page object for the single active docs page in Storybook 44 | */ 45 | export class DocsPageObject { 46 | assert = new Assertions(this); 47 | 48 | action = new Actions(this); 49 | 50 | waitUntil = new Waits(this); 51 | 52 | argsTable = new StorybookArgsTableObject({ 53 | rootLocator: this.docsRoot.locator(".docblock-argstable"), 54 | descriptionColumnIndex: 1, 55 | }); 56 | 57 | get previewIframeLocator() { 58 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 59 | } 60 | 61 | get previewLoader() { 62 | return this.page.locator("#preview-loader"); 63 | } 64 | 65 | get docsRoot() { 66 | return this.previewIframeLocator.locator("#storybook-docs"); 67 | } 68 | 69 | constructor(public page: Page) {} 70 | } 71 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/tests/objects/DocsPageObject.ts: -------------------------------------------------------------------------------- 1 | import {expect, type Page} from "playwright/test"; 2 | import type {ControlExpectation} from "../types"; 3 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 4 | 5 | class Waits { 6 | constructor(private object: DocsPageObject) {} 7 | 8 | async previewIframeLoaded() { 9 | await this.object.previewIframeLocator.owner().isVisible(); 10 | await this.object.previewLoader.isHidden(); 11 | 12 | // wait for iframe to have attribute 13 | await this.object.page.waitForSelector( 14 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 15 | {state: "visible"}, 16 | ); 17 | 18 | // make sure docs loaded 19 | await this.object.docsRoot.waitFor({state: "visible"}); 20 | } 21 | } 22 | 23 | class Assertions { 24 | constructor(private object: DocsPageObject) {} 25 | 26 | async actualConfigMatches(expectedConfig: Record) { 27 | const actualConfigText = await this.object.docsRoot // story is rendered but not visible, so here we are specifying the docs root 28 | .locator("#actual-config-json") 29 | .innerText(); 30 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 31 | } 32 | 33 | async controlsMatch(expectedControlsMap: Record) { 34 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 35 | } 36 | } 37 | 38 | class Actions { 39 | constructor(private object: DocsPageObject) {} 40 | } 41 | 42 | /** 43 | * Page object for the single active docs page in Storybook 44 | */ 45 | export class DocsPageObject { 46 | assert = new Assertions(this); 47 | 48 | action = new Actions(this); 49 | 50 | waitUntil = new Waits(this); 51 | 52 | argsTable = new StorybookArgsTableObject({ 53 | rootLocator: this.docsRoot.locator(".docblock-argstable"), 54 | descriptionColumnIndex: 1, 55 | }); 56 | 57 | get previewIframeLocator() { 58 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 59 | } 60 | 61 | get previewLoader() { 62 | return this.page.locator("#preview-loader"); 63 | } 64 | 65 | get docsRoot() { 66 | return this.previewIframeLocator.locator("#storybook-docs"); 67 | } 68 | 69 | constructor(public page: Page) {} 70 | } 71 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/tests/objects/DocsPageObject.ts: -------------------------------------------------------------------------------- 1 | import {expect, type Page} from "playwright/test"; 2 | import type {ControlExpectation} from "../types"; 3 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 4 | 5 | class Waits { 6 | constructor(private object: DocsPageObject) {} 7 | 8 | async previewIframeLoaded() { 9 | await this.object.previewIframeLocator.owner().isVisible(); 10 | await this.object.previewLoader.isHidden(); 11 | 12 | // wait for iframe to have attribute 13 | await this.object.page.waitForSelector( 14 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 15 | {state: "visible"}, 16 | ); 17 | 18 | // make sure docs loaded 19 | await this.object.docsRoot.waitFor({state: "visible"}); 20 | } 21 | } 22 | 23 | class Assertions { 24 | constructor(private object: DocsPageObject) {} 25 | 26 | async actualConfigMatches(expectedConfig: Record) { 27 | const actualConfigText = await this.object.docsRoot // story is rendered but not visible, so here we are specifying the docs root 28 | .locator("#actual-config-json") 29 | .innerText(); 30 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 31 | } 32 | 33 | async controlsMatch(expectedControlsMap: Record) { 34 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 35 | } 36 | } 37 | 38 | class Actions { 39 | constructor(private object: DocsPageObject) {} 40 | } 41 | 42 | /** 43 | * Page object for the single active docs page in Storybook 44 | */ 45 | export class DocsPageObject { 46 | assert = new Assertions(this); 47 | 48 | action = new Actions(this); 49 | 50 | waitUntil = new Waits(this); 51 | 52 | argsTable = new StorybookArgsTableObject({ 53 | rootLocator: this.docsRoot.locator(".docblock-argstable"), 54 | descriptionColumnIndex: 1, 55 | }); 56 | 57 | get previewIframeLocator() { 58 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 59 | } 60 | 61 | get previewLoader() { 62 | return this.page.locator("#preview-loader"); 63 | } 64 | 65 | get docsRoot() { 66 | return this.previewIframeLocator.locator("#storybook-docs"); 67 | } 68 | 69 | constructor(public page: Page) {} 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mono-repo-root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "upgrade": "yarn upgrade-interactive && yarn --cwd packages-prod/example run upgrade", 9 | "test": "npm --prefix packages/addon run test && npm --prefix packages/example-v8 run test", 10 | "lint": "eslint --report-unused-disable-directives --cache --fix --quiet --ext .ts,.tsx --ignore-path .gitignore .", 11 | "check-types": "tsc --noEmit", 12 | "format": "prettier --write --cache --ignore-path .gitignore . \"!**/.yarn/**\"", 13 | "check": "concurrently --timings --prefix-colors auto --kill-others-on-fail \"npm run lint\" \"npm run check-types\" \"npm run format\"", 14 | "build:addon": "npm --prefix packages/addon run build", 15 | "addon:check-exports": "npm --prefix packages/addon run check-exports", 16 | "addon:test": "npm --prefix packages/addon run test", 17 | "example:deploy": "npm --prefix packages/example-v10-vite run deploy", 18 | "prepublish:base": "npm run build:addon && concurrently --timings --prefix-colors auto --kill-others-on-fail \"npm run lint\" \"npm run check-types\" \"npm run format\" \"npm run addon:test\" \"npm run addon:check-exports\" \"npm --prefix packages/example-v9-vite run test\" \"npm --prefix packages/example-v10-vite run test\" \"npm run sync-github-files\"", 19 | "publish:base": "node --experimental-strip-types ./scripts/publish.ts relativeGitPath=. relativeNpmPath=./packages/addon commitChanges=true", 20 | "publish:patch": "npm run publish:base -- level=patch", 21 | "publish:minor": "npm run publish:base -- level=minor", 22 | "publish:major": "npm run publish:base -- level=major", 23 | "sync-github-files": "node ./scripts/sync-github-files.js", 24 | "install:playwright-browsers": "playwright install --with-deps" 25 | }, 26 | "devDependencies": { 27 | "@eliasm307/config": "0.51.1", 28 | "@playwright/test": "1.53.1", 29 | "@types/node": "^22.15.32", 30 | "@types/react": "18.3.23", 31 | "@types/react-dom": "18.3.7", 32 | "@typescript-eslint/eslint-plugin": "^7.18.0", 33 | "@typescript-eslint/parser": "^7.18.0", 34 | "concurrently": "^9.1.2", 35 | "eslint": "^8.57.1", 36 | "eslint-config-next": "13.5.11", 37 | "eslint-plugin-import": "^2.31.0", 38 | "eslint-plugin-storybook": "^0.9.0", 39 | "netlify-cli": "^22.1.3", 40 | "playwright": "1.53.1", 41 | "react": "18.3.1", 42 | "react-dom": "18.3.1", 43 | "typescript": "5.8.3" 44 | }, 45 | "packageManager": "yarn@4.9.2" 46 | } 47 | -------------------------------------------------------------------------------- /packages/addon/index.d.ts: -------------------------------------------------------------------------------- 1 | // NOTE: we dont import from storybook here so types are version agnostic 2 | 3 | type DeepControlsAddonParameters = { 4 | /** 5 | * Whether the deep controls addon is enabled. 6 | * 7 | * This can be enabled/disabled at the Meta level to apply to all stories, 8 | * or granularly at the story level to apply to a single story. 9 | * 10 | * @default false 11 | */ 12 | enabled?: boolean; 13 | }; 14 | 15 | /** 16 | * This is to prevent auto complete being broken if type uses mapped types 17 | * 18 | * @internal 19 | */ 20 | type RemappedOmit = { 21 | [P in keyof T as Exclude]: T[P]; 22 | }; 23 | 24 | type GenericStrictInputType = {type?: unknown}; 25 | 26 | type ValuesOf = T extends object ? T[keyof T] : never; 27 | 28 | /** 29 | * Utility type to extend Storybook Story and Meta types with deep controls parameters 30 | * and update `argTypes` typing to allow for deep-controls usages. 31 | * 32 | * @example 33 | * ```ts 34 | * // Type is wrapped over the StoryType 35 | * const meta: TypeWithDeepControls = { 36 | * argTypes: { 37 | * // no type error 38 | * "someObject.enumString": { 39 | * control: "string", 40 | * }, 41 | * }, 42 | * // Type is wrapped over the MetaType 43 | * }; 44 | * 45 | * export default meta; 46 | * 47 | * type Story = TypeWithDeepControls; 48 | * 49 | * export const SomeStory: Story = { 50 | * args: { 51 | * someObject: { 52 | * anyString: "string", 53 | * enumString: "string", 54 | * }, 55 | * }, 56 | * argTypes: { 57 | * // no type error 58 | * "someObject.enumString": { 59 | * control: "radio", 60 | * options: ["value1", "value2", "value3"], 61 | * }, 62 | * }, 63 | * }; 64 | * ``` 65 | */ 66 | export type TypeWithDeepControls< 67 | TStoryOrMeta extends { 68 | argTypes?: Partial>; 69 | parameters?: Record; 70 | }, 71 | > = TStoryOrMeta & { 72 | // custom argTypes for deep controls only, loosens the key type to allow for deep control keys 73 | argTypes?: Record< 74 | `${string}.${string}`, 75 | // NOTE: partial here because the arg type input configs will be merged with the injected deep control arg types so we make sure we support partial config, 76 | // the type is already partial currently so making it partial is so if the type becomes strict in the future we still support it 77 | Partial> 78 | >; 79 | parameters?: TStoryOrMeta["parameters"] & { 80 | deepControls?: DeepControlsAddonParameters; 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/addon/src/utils/general.ts: -------------------------------------------------------------------------------- 1 | export function setProperty>>( 2 | object: T, 3 | path: string, 4 | value: any, 5 | ): T { 6 | if (!isAnyObject(object)) { 7 | return object; // should be an object but handle if it isn't 8 | } 9 | 10 | const remainingPathSegments = path.split("."); 11 | const currentTargetSegment = remainingPathSegments.shift(); 12 | if (!currentTargetSegment) { 13 | return object; // invalid path ignore 14 | } 15 | 16 | if (!remainingPathSegments.length) { 17 | // we have reached the last segment so set the value 18 | object[currentTargetSegment as keyof T] = value; 19 | return object; 20 | } 21 | // we have more segments to go so recurse if possible 22 | 23 | let nextTargetObj = object[currentTargetSegment]; 24 | if (nextTargetObj === undefined) { 25 | // next target doesn't exist so create one in our path 26 | object[currentTargetSegment as keyof T] = {} as any; 27 | nextTargetObj = object[currentTargetSegment]; 28 | 29 | // check if we can go further 30 | } else if (!nextTargetObj || typeof nextTargetObj !== "object") { 31 | return object; // cant go further, invalid path ignore the rest 32 | } 33 | 34 | // recurse 35 | setProperty(nextTargetObj as object, remainingPathSegments.join("."), value); 36 | 37 | // need to return the original object, only the top level caller will get this 38 | return object; 39 | } 40 | 41 | export function getProperty(value: unknown, path: string): unknown { 42 | for (const pathSegment of path.split(".")) { 43 | if (!isAnyObject(value)) { 44 | return; // cant go further, invalid path ignore 45 | } 46 | if (pathSegment in value) { 47 | value = value[pathSegment]; 48 | } else { 49 | return; // invalid path ignore 50 | } 51 | } 52 | 53 | return value; // value at the end of the path 54 | } 55 | 56 | const POJO_PROTOTYPES = [Object.prototype, null]; 57 | 58 | /** 59 | * Is the value a simple object, ie a Plain Old Javascript Object, 60 | * not a class instance, function, array etc which are also objects 61 | * 62 | * @internal 63 | */ 64 | export function isPojo(val: unknown): val is Record { 65 | return Boolean( 66 | typeof val === "object" && 67 | val && 68 | POJO_PROTOTYPES.includes(Object.getPrototypeOf(val)) && 69 | !isReactElement(val), 70 | ); 71 | } 72 | 73 | function isAnyObject(val: unknown): val is Record { 74 | return typeof val === "object" && val !== null; 75 | } 76 | 77 | // NOTE: React has `#isValidElement` utility to check this, however we dont use it here so React isn't a dependency 78 | export function isReactElement(val: Record): boolean { 79 | return typeof val.$$typeof === "symbol"; 80 | } 81 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/tests/WithTypedProps.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import StorybookPageObject from "./utils/StorybookPage"; 3 | import {localHostPortIsInUse} from "./utils"; 4 | import {STORYBOOK_V7_PORT} from "./utils/constants"; 5 | 6 | test.beforeAll(async () => { 7 | const isStorybookRunning = await localHostPortIsInUse(STORYBOOK_V7_PORT); 8 | if (!isStorybookRunning) { 9 | throw new Error( 10 | `Storybook is not running (expected on localhost:${STORYBOOK_V7_PORT}), please run 'npm run storybook' in a separate terminal`, 11 | ); 12 | } 13 | }); 14 | 15 | test.beforeEach(async ({page}) => { 16 | test.setTimeout(60_000); 17 | await new StorybookPageObject(page).openPage(); 18 | }); 19 | 20 | test("shows default controls when initial values are not defined", async ({page}) => { 21 | const storybookPage = new StorybookPageObject(page); 22 | await storybookPage.action.clickStoryById("stories-withtypedprops--default-enabled"); 23 | await storybookPage.assert.controlsMatch({ 24 | someString: undefined, 25 | someObject: undefined, 26 | someArray: undefined, 27 | }); 28 | await storybookPage.assert.actualConfigMatches({}); 29 | }); 30 | 31 | test("shows deep controls when initial values are defined", async ({page}) => { 32 | const storybookPage = new StorybookPageObject(page); 33 | await storybookPage.action.clickStoryById("stories-withtypedprops--with-args"); 34 | await storybookPage.assert.controlsMatch({ 35 | someString: undefined, // still included 36 | "someObject.anyString": "anyString", 37 | "someObject.enumString": "enumString", 38 | someArray: [], // just represents a complex control 39 | }); 40 | await storybookPage.assert.actualConfigMatches({ 41 | someArray: ["string1", "string2"], 42 | someObject: { 43 | anyString: "anyString", 44 | enumString: "enumString", 45 | }, 46 | }); 47 | }); 48 | 49 | test("supports customising controls with initial values", async ({page}) => { 50 | const storybookPage = new StorybookPageObject(page); 51 | await storybookPage.action.clickStoryById("stories-withtypedprops--with-custom-controls"); 52 | await storybookPage.assert.controlsMatch({ 53 | someString: { 54 | type: "radio", 55 | options: ["string1", "string2", "string3"], 56 | }, 57 | "someObject.anyString": "anyString", 58 | "someObject.enumString": { 59 | type: "radio", 60 | options: ["value1", "value2", "value3"], 61 | }, 62 | someArray: [], // just represents a complex control 63 | }); 64 | 65 | // initial value not affected by custom controls 66 | await storybookPage.assert.actualConfigMatches({ 67 | someArray: ["string1", "string2"], 68 | someObject: { 69 | anyString: "anyString", 70 | enumString: "enumString", 71 | }, 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/addon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-deep-controls", 3 | "version": "0.10.0", 4 | "description": "A Storybook addon that extends @storybook/addon-controls and provides an alternative to interacting with object arguments", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/eliasm307/storybook-addon-deep-controls.git" 8 | }, 9 | "keywords": [ 10 | "storybook", 11 | "addon", 12 | "controls", 13 | "storybook-addon" 14 | ], 15 | "author": "Elias Mangoro", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/eliasm307/storybook-addon-deep-controls/issues" 19 | }, 20 | "homepage": "https://github.com/eliasm307/storybook-addon-deep-controls#readme", 21 | "files": [ 22 | "dist", 23 | "preset.js", 24 | "register.js", 25 | "register.mjs", 26 | "preview.js", 27 | "preview.mjs", 28 | "index.d.ts", 29 | "index.js", 30 | "index.mjs" 31 | ], 32 | "main": "index.js", 33 | "module": "index.mjs", 34 | "types": "index.d.ts", 35 | "exportsNotes": [ 36 | "- The order of the keys matters and represents the order of preference e.g. if 'import' is first then it is the preferred import method", 37 | "'types' needs to be first", 38 | "", 39 | "- See example at https://github.com/storybookjs/addon-kit/blob/main/package.json" 40 | ], 41 | "exports": { 42 | ".": { 43 | "types": "./index.d.ts", 44 | "import": "./index.mjs", 45 | "require": "./index.js" 46 | }, 47 | "./preview": { 48 | "types": "./index.d.ts", 49 | "import": "./preview.mjs", 50 | "require": "./preview.js", 51 | "default": "./preview.mjs" 52 | }, 53 | "./preview.js": { 54 | "types": "./index.d.ts", 55 | "import": "./preview.mjs", 56 | "require": "./preview.js", 57 | "default": "./preview.mjs" 58 | }, 59 | "./package.json": "./package.json", 60 | "./register": "./register.js", 61 | "./register.js": "./register.js" 62 | }, 63 | "scripts": { 64 | "prepare": "npm run build", 65 | "build": "concurrently --timings --prefix-colors auto --kill-others-on-fail \"tsc -p tsconfig.build.esm.json\" \"tsc -p tsconfig.build.cjs.json\"", 66 | "test": "vitest run", 67 | "test:watch": "vitest", 68 | "check-exports": "attw --pack . --ignore-rules untyped-resolution false-cjs", 69 | "build-pack": "npm run build && npm pack --pack-destination=dist" 70 | }, 71 | "peerDependencies": { 72 | "storybook": ">= 7.0.0 < 8.5.0 || >= 9.0.0 < 11.0.0" 73 | }, 74 | "devDependencies": { 75 | "@arethetypeswrong/cli": "^0.18.2", 76 | "@vitest/coverage-v8": "^3.2.4", 77 | "react": "18.3.1", 78 | "storybook": "10.0.2", 79 | "vitest": "^3.2.4" 80 | }, 81 | "installConfig": { 82 | "hoistingLimits": "workspaces" 83 | } 84 | } -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | // NOTE: some utils duplicated from addon package as they are not exposed in the public API 2 | 3 | const POJO_PROTOTYPES = [Object.prototype, null]; 4 | 5 | /** 6 | * Is the value a simple object, ie a Plain Old Javascript Object, 7 | * not a class instance, function, array etc which are also objects 8 | */ 9 | export function isPojo(val: unknown): val is Record { 10 | return Boolean( 11 | typeof val === "object" && 12 | val && 13 | POJO_PROTOTYPES.includes(Object.getPrototypeOf(val)) && 14 | !isReactElement(val), 15 | ); 16 | } 17 | 18 | // NOTE: React has `#isValidElement` utility to check this, however we dont use it here so React isn't a dependency 19 | export function isReactElement(val: Record): boolean { 20 | return typeof val.$$typeof === "symbol"; 21 | } 22 | 23 | export function stringify(data: unknown): string { 24 | return JSON.stringify(data, replacer, 2); 25 | } 26 | 27 | function replacer(inputKey: string, inputValue: unknown): unknown { 28 | if (inputValue === undefined) { 29 | return "[undefined]"; 30 | } 31 | 32 | if (typeof inputValue === "number") { 33 | if (isNaN(inputValue)) { 34 | return "[NaN]"; 35 | } 36 | 37 | if (!isFinite(inputValue)) { 38 | return "[Infinity]"; 39 | } 40 | } 41 | 42 | // any falsy values can be serialised? 43 | if (!inputValue) { 44 | return inputValue; 45 | } 46 | 47 | if (inputValue instanceof Error) { 48 | return `[Error("${inputValue.message}")]`; 49 | } 50 | 51 | if (typeof inputValue === "function") { 52 | return `[Function:${inputValue.name || "anonymous"}]`; 53 | } 54 | 55 | if (typeof inputValue === "symbol") { 56 | return `[${inputValue.toString()}]`; 57 | } 58 | 59 | if (inputValue instanceof Promise) { 60 | return "[Promise]"; 61 | } 62 | 63 | if (inputValue instanceof Map) { 64 | const normalisedMap: Record = {}; 65 | for (const [key, value] of inputValue.entries()) { 66 | normalisedMap[key] = value; 67 | } 68 | return normalisedMap; 69 | } 70 | 71 | if (inputValue instanceof RegExp) { 72 | return inputValue.toString(); 73 | } 74 | 75 | if (inputValue instanceof Set) { 76 | return Array.from(inputValue); 77 | } 78 | 79 | if (Array.isArray(inputValue)) { 80 | return inputValue.map((value, index) => { 81 | return replacer(`${inputKey}[${index}]`, value); 82 | }); 83 | } 84 | 85 | if (typeof inputValue === "object") { 86 | if (isReactElement(inputValue)) { 87 | return "[ReactElement]"; 88 | } 89 | 90 | const isPojoValue = isPojo(inputValue); 91 | if (isPojoValue) { 92 | return inputValue; 93 | } 94 | 95 | const className = inputValue.constructor.name; 96 | return `[${className}]`; 97 | } 98 | 99 | return inputValue; 100 | } 101 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yaml: -------------------------------------------------------------------------------- 1 | # see https://medium.com/@nickjabs/running-github-actions-in-parallel-and-sequentially-b338e4a46bf5 2 | # see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks 3 | 4 | name: pr-check 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | # see https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | # NOTE: its faster to run everything in one job for now rather than parallel jobs, 17 | # where the overhead of starting a new job and installing deps etc is not worth it 18 | pr-check-main-job: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Check out Git repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Use Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: "20" 28 | registry-url: "https://registry.npmjs.org" 29 | cache: "yarn" 30 | 31 | - name: Install dependencies 32 | run: yarn install --immutable 33 | 34 | - name: Run exports check 35 | run: npm run --prefix packages/addon check-exports 36 | 37 | - name: Run lint 38 | run: npm run lint 39 | 40 | - name: Run type check 41 | run: npm run check-types 42 | 43 | - name: Run format check 44 | run: npm run format 45 | 46 | - name: Run addon unit tests 47 | run: npm run --prefix packages/addon test 48 | 49 | # E2E TESTS 50 | 51 | - name: Get installed Playwright version 52 | id: playwright-version 53 | run: echo "PLAYWRIGHT_VERSION=$(npx playwright --version)" >> $GITHUB_ENV 54 | 55 | # see https://dev.to/ayomiku222/how-to-cache-playwright-browser-on-github-actions-51o6 56 | - name: Cache Playwright binaries 57 | uses: actions/cache@v3 58 | id: cache-playwright 59 | with: 60 | path: ~/.cache/ms-playwright 61 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 62 | 63 | # see https://playwright.dev/docs/ci-intro 64 | - name: Install Playwright Browsers 65 | if: steps.cache-playwright.outputs.cache-hit != 'true' 66 | run: npx playwright install --with-deps 67 | 68 | - name: Run Storybook v10-vite e2e tests 69 | run: npm run --prefix packages/example-v10-vite test 70 | 71 | - name: Run Storybook v9-vite e2e tests 72 | run: npm run --prefix packages/example-v9-vite test 73 | 74 | - name: Run Storybook v8-vite e2e tests 75 | run: npm run --prefix packages/example-v8-vite test 76 | 77 | - name: Run Storybook v8-webpack e2e tests 78 | run: npm run --prefix packages/example-v8-webpack test 79 | 80 | - name: Run Storybook v7-webpack e2e tests 81 | run: npm run --prefix packages/example-v7-webpack test 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | **/.yarn/cache 124 | **/.yarn/unplugged 125 | **/.yarn/build-state.yml 126 | **/.yarn/install-state.gz 127 | **/*.pnp.* 128 | 129 | **/storybook-static/ 130 | # Local Netlify folder 131 | .netlify 132 | 133 | **/playwright-report/ 134 | *storybook.log 135 | 136 | # local env files 137 | **/.env*.local 138 | 139 | # vercel 140 | **/.vercel 141 | 142 | # typescript 143 | **/*.tsbuildinfo 144 | 145 | **/next-env.d.ts 146 | 147 | **/test-results/ 148 | **/playwright-report/ 149 | **/playwright/.cache/ 150 | **/.last-run.json -------------------------------------------------------------------------------- /packages/example-v8-generic/src/tests/objects/StoryPageObject.ts: -------------------------------------------------------------------------------- 1 | import {type Page, expect} from "playwright/test"; 2 | import {setTimeout} from "timers/promises"; 3 | import type {ControlExpectation} from "../types"; 4 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 5 | 6 | // todo extract magic storybook id/class etc selectors to constants 7 | class Assertions { 8 | constructor(private object: StoryPageObject) {} 9 | 10 | async actualConfigMatches(expectedConfig: Record) { 11 | await setTimeout(1000); // wait for change to be applied, reduces flakiness 12 | const actualConfigText = await this.object.previewIframeLocator 13 | .locator("#storybook-root") // docs are rendered but not visible, so here we are specifying the main story root 14 | .locator("#actual-config-json") 15 | .innerText(); 16 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 17 | } 18 | 19 | async controlsMatch(expectedControlsMap: Record) { 20 | // check controls count to make sure we are not missing any 21 | const actualControlsAddonTabTitle = await this.object.addonPanelTabsLocator.textContent(); 22 | const expectedControlEntries = Object.entries(expectedControlsMap); 23 | expect(actualControlsAddonTabTitle?.trim(), "controls tab title equals").toEqual( 24 | `Controls${expectedControlEntries.length}`, 25 | ); 26 | 27 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 28 | } 29 | } 30 | 31 | class Actions { 32 | constructor(private object: StoryPageObject) {} 33 | } 34 | 35 | class Waits { 36 | constructor(private object: StoryPageObject) {} 37 | 38 | async previewIframeLoaded() { 39 | await this.object.previewIframeLocator.owner().isVisible(); 40 | await this.object.previewLoader.isHidden(); 41 | 42 | // wait for iframe to have attribute 43 | await this.object.page.waitForSelector( 44 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 45 | {state: "visible"}, 46 | ); 47 | 48 | // make sure controls loaded 49 | await this.object.addonsPanelLocator 50 | .locator("#panel-tab-content .docblock-argstable") 51 | .waitFor({state: "visible"}); 52 | } 53 | } 54 | 55 | /** 56 | * Page object for the single active story in Storybook 57 | */ 58 | export class StoryPageObject { 59 | assert = new Assertions(this); 60 | 61 | action = new Actions(this); 62 | 63 | waitUntil = new Waits(this); 64 | 65 | argsTable = new StorybookArgsTableObject({ 66 | rootLocator: this.addonsPanelLocator.locator(".docblock-argstable"), 67 | }); 68 | 69 | get resetControlsButtonLocator() { 70 | return this.page.getByRole("button", {name: "Reset controls"}); 71 | } 72 | 73 | get addonsPanelLocator() { 74 | return this.page.locator("#storybook-panel-root"); 75 | } 76 | 77 | get previewLoader() { 78 | return this.page.locator("#preview-loader"); 79 | } 80 | 81 | get addonPanelTabsLocator() { 82 | return this.page.locator("#tabbutton-addon-controls"); 83 | } 84 | 85 | get previewIframeLocator() { 86 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 87 | } 88 | 89 | constructor(public page: Page) {} 90 | } 91 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/tests/objects/StoryPageObject.ts: -------------------------------------------------------------------------------- 1 | import {type Page, expect} from "playwright/test"; 2 | import {setTimeout} from "node:timers/promises"; 3 | import type {ControlExpectation} from "../types"; 4 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 5 | 6 | // todo extract magic storybook id/class etc selectors to constants 7 | class Assertions { 8 | constructor(private object: StoryPageObject) {} 9 | 10 | async actualConfigMatches(expectedConfig: Record) { 11 | await setTimeout(1000); // wait for change to be applied, reduces flakiness 12 | const actualConfigText = await this.object.previewIframeLocator 13 | .locator("#storybook-root") // docs are rendered but not visible, so here we are specifying the main story root 14 | .locator("#actual-config-json") 15 | .innerText(); 16 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 17 | } 18 | 19 | async controlsMatch(expectedControlsMap: Record) { 20 | // check controls count to make sure we are not missing any 21 | const actualControlsAddonTabTitle = await this.object.addonPanelTabsLocator.textContent(); 22 | const expectedControlEntries = Object.entries(expectedControlsMap); 23 | expect(actualControlsAddonTabTitle?.trim(), "controls tab title equals").toEqual( 24 | `Controls${expectedControlEntries.length}`, 25 | ); 26 | 27 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 28 | } 29 | } 30 | 31 | class Actions { 32 | constructor(private object: StoryPageObject) {} 33 | } 34 | 35 | class Waits { 36 | constructor(private object: StoryPageObject) {} 37 | 38 | async previewIframeLoaded() { 39 | await this.object.previewIframeLocator.owner().isVisible(); 40 | await this.object.previewLoader.isHidden(); 41 | 42 | // wait for iframe to have attribute 43 | await this.object.page.waitForSelector( 44 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 45 | {state: "visible"}, 46 | ); 47 | 48 | // make sure controls loaded 49 | await this.object.addonsPanelLocator 50 | .locator("#panel-tab-content .docblock-argstable") 51 | .waitFor({state: "visible"}); 52 | } 53 | } 54 | 55 | /** 56 | * Page object for the single active story in Storybook 57 | */ 58 | export class StoryPageObject { 59 | assert = new Assertions(this); 60 | 61 | action = new Actions(this); 62 | 63 | waitUntil = new Waits(this); 64 | 65 | argsTable = new StorybookArgsTableObject({ 66 | rootLocator: this.addonsPanelLocator.locator(".docblock-argstable"), 67 | }); 68 | 69 | get resetControlsButtonLocator() { 70 | return this.page.getByRole("button", {name: "Reset controls"}); 71 | } 72 | 73 | get addonsPanelLocator() { 74 | return this.page.locator("#storybook-panel-root"); 75 | } 76 | 77 | get previewLoader() { 78 | return this.page.locator("#preview-loader"); 79 | } 80 | 81 | get addonPanelTabsLocator() { 82 | return this.page.locator("#tabbutton-addon-controls"); 83 | } 84 | 85 | get previewIframeLocator() { 86 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 87 | } 88 | 89 | constructor(public page: Page) {} 90 | } 91 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/tests/objects/StoryPageObject.ts: -------------------------------------------------------------------------------- 1 | import {type Page, expect} from "playwright/test"; 2 | import {setTimeout} from "node:timers/promises"; 3 | import type {ControlExpectation} from "../types"; 4 | import {StorybookArgsTableObject} from "./ArgsTableObject"; 5 | 6 | // todo extract magic storybook id/class etc selectors to constants 7 | class Assertions { 8 | constructor(private object: StoryPageObject) {} 9 | 10 | async actualConfigMatches(expectedConfig: Record) { 11 | await setTimeout(1000); // wait for change to be applied, reduces flakiness 12 | const actualConfigText = await this.object.previewIframeLocator 13 | .locator("#storybook-root") // docs are rendered but not visible, so here we are specifying the main story root 14 | .locator("#actual-config-json") 15 | .innerText(); 16 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 17 | } 18 | 19 | async controlsMatch(expectedControlsMap: Record) { 20 | // check controls count to make sure we are not missing any 21 | const actualControlsAddonTabTitle = await this.object.addonPanelTabsLocator.textContent(); 22 | const expectedControlEntries = Object.entries(expectedControlsMap); 23 | expect(actualControlsAddonTabTitle?.trim(), "controls tab title equals").toEqual( 24 | `Controls${expectedControlEntries.length}`, 25 | ); 26 | 27 | await this.object.argsTable.assert.controlsMatch(expectedControlsMap); 28 | } 29 | } 30 | 31 | class Actions { 32 | constructor(private object: StoryPageObject) {} 33 | } 34 | 35 | class Waits { 36 | constructor(private object: StoryPageObject) {} 37 | 38 | async previewIframeLoaded() { 39 | await this.object.previewIframeLocator.owner().isVisible(); 40 | await this.object.previewLoader.isHidden(); 41 | 42 | // wait for iframe to have attribute 43 | await this.object.page.waitForSelector( 44 | `iframe[title="storybook-preview-iframe"][data-is-loaded="true"]`, 45 | {state: "visible"}, 46 | ); 47 | 48 | // make sure controls loaded 49 | await this.object.addonsPanelLocator 50 | .locator("#panel-tab-content .docblock-argstable") 51 | .waitFor({state: "visible"}); 52 | } 53 | } 54 | 55 | /** 56 | * Page object for the single active story in Storybook 57 | */ 58 | export class StoryPageObject { 59 | assert = new Assertions(this); 60 | 61 | action = new Actions(this); 62 | 63 | waitUntil = new Waits(this); 64 | 65 | argsTable = new StorybookArgsTableObject({ 66 | rootLocator: this.addonsPanelLocator.locator(".docblock-argstable"), 67 | }); 68 | 69 | get resetControlsButtonLocator() { 70 | return this.page.getByRole("button", {name: "Reset controls"}); 71 | } 72 | 73 | get addonsPanelLocator() { 74 | return this.page.locator("#storybook-panel-root"); 75 | } 76 | 77 | get previewLoader() { 78 | return this.page.locator("#preview-loader"); 79 | } 80 | 81 | get addonPanelTabsLocator() { 82 | return this.page.locator("#tabbutton-addon-controls"); 83 | } 84 | 85 | get previewIframeLocator() { 86 | return this.page.frameLocator(`iframe[title="storybook-preview-iframe"]`); 87 | } 88 | 89 | constructor(public page: Page) {} 90 | } 91 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/tests/objects/AppObject.ts: -------------------------------------------------------------------------------- 1 | import type {Page} from "@playwright/test"; 2 | import {expect} from "@playwright/test"; 3 | import {STORYBOOK_PORT} from "../../utils/constants"; 4 | import {DocsPageObject} from "./DocsPageObject"; 5 | import {StoryPageObject} from "./StoryPageObject"; 6 | 7 | class Assertions { 8 | constructor(private object: AppObject) {} 9 | 10 | /** 11 | * 12 | * @param expectedStoryId Can be story or Docs id 13 | */ 14 | async activePageIdEquals(expectedStoryId: string) { 15 | const actualId = await this.object.storiesTreeLocator.getAttribute("data-highlighted-item-id"); 16 | expect(actualId, {message: "active story id"}).toEqual(expectedStoryId); 17 | } 18 | } 19 | 20 | class Actions { 21 | constructor(private object: AppObject) {} 22 | 23 | /** 24 | * 25 | * @param id Story id, e.g. "stories-dev--enabled" 26 | * 27 | * @note This is the ID shown in the URL when you click on a story in the Storybook UI e.g. 28 | * `http://localhost:${STORYBOOK_PORT}/?path=/story/stories-dev--enabled` 29 | */ 30 | async openStoriesTreeItemById(type: "story" | "docs", id: `stories-${string}--${string}`) { 31 | await this.clickStoryTreeItemById(id); 32 | 33 | // wait until loaded 34 | switch (type) { 35 | case "story": 36 | return this.object.activeStoryPage.waitUntil.previewIframeLoaded(); 37 | case "docs": 38 | return this.object.activeDocsPage.waitUntil.previewIframeLoaded(); 39 | default: 40 | throw Error(`Invalid tree item type: ${type}`); 41 | } 42 | } 43 | 44 | private async clickStoryTreeItemById(id: string) { 45 | if (!id.includes("--")) { 46 | throw new Error( 47 | `Invalid story id, ${id}, it should include "--" to separate the component and story id`, 48 | ); 49 | } 50 | const componentId = id.split("--")[0]; 51 | const storyIsVisible = await this.object.storiesTreeLocator.locator(`#${id}`).isVisible(); 52 | if (!storyIsVisible) { 53 | await this.object.storiesTreeLocator.locator(`#${componentId}`).click(); // make sure the component is expanded 54 | } 55 | await this.object.storiesTreeLocator.locator(`#${id}`).click(); 56 | await this.object.assert.activePageIdEquals(id); 57 | } 58 | } 59 | 60 | class Waits { 61 | constructor(private object: AppObject) {} 62 | } 63 | 64 | export class AppObject { 65 | assert = new Assertions(this); 66 | 67 | action = new Actions(this); 68 | 69 | waitUntil = new Waits(this); 70 | 71 | activeStoryPage = new StoryPageObject(this.page); 72 | 73 | activeDocsPage = new DocsPageObject(this.page); 74 | 75 | constructor(public page: Page) {} 76 | 77 | async openDefaultPage() { 78 | const STORYBOOK_URL = `http://localhost:${STORYBOOK_PORT}/?path=/story/stories-dev--enabled`; 79 | 80 | try { 81 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 82 | } catch { 83 | // sometimes goto times out, so try again 84 | 85 | console.warn("page.goto timed out, trying again"); 86 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 87 | } 88 | 89 | await this.activeStoryPage.waitUntil.previewIframeLoaded(); 90 | } 91 | 92 | get storiesTreeLocator() { 93 | return this.page.locator("#storybook-explorer-tree"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/tests/objects/AppObject.ts: -------------------------------------------------------------------------------- 1 | import type {Page} from "@playwright/test"; 2 | import {expect} from "@playwright/test"; 3 | import {STORYBOOK_PORT} from "../../utils/constants"; 4 | import {DocsPageObject} from "./DocsPageObject"; 5 | import {StoryPageObject} from "./StoryPageObject"; 6 | 7 | class Assertions { 8 | constructor(private object: AppObject) {} 9 | 10 | /** 11 | * 12 | * @param expectedStoryId Can be story or Docs id 13 | */ 14 | async activePageIdEquals(expectedStoryId: string) { 15 | const actualId = await this.object.storiesTreeLocator.getAttribute("data-highlighted-item-id"); 16 | expect(actualId, {message: "active story id"}).toEqual(expectedStoryId); 17 | } 18 | } 19 | 20 | class Actions { 21 | constructor(private object: AppObject) {} 22 | 23 | /** 24 | * 25 | * @param id Story id, e.g. "stories-dev--enabled" 26 | * 27 | * @note This is the ID shown in the URL when you click on a story in the Storybook UI e.g. 28 | * `http://localhost:${STORYBOOK_V8_PORT}/?path=/story/stories-dev--enabled` 29 | */ 30 | async openStoriesTreeItemById(type: "story" | "docs", id: `stories-${string}--${string}`) { 31 | await this.clickStoryTreeItemById(id); 32 | 33 | // wait until loaded 34 | switch (type) { 35 | case "story": 36 | return this.object.activeStoryPage.waitUntil.previewIframeLoaded(); 37 | case "docs": 38 | return this.object.activeDocsPage.waitUntil.previewIframeLoaded(); 39 | default: 40 | throw Error(`Invalid tree item type: ${type}`); 41 | } 42 | } 43 | 44 | private async clickStoryTreeItemById(id: string) { 45 | if (!id.includes("--")) { 46 | throw new Error( 47 | `Invalid story id, ${id}, it should include "--" to separate the component and story id`, 48 | ); 49 | } 50 | const componentId = id.split("--")[0]; 51 | const storyIsVisible = await this.object.storiesTreeLocator.locator(`#${id}`).isVisible(); 52 | if (!storyIsVisible) { 53 | await this.object.storiesTreeLocator.locator(`#${componentId}`).click(); // make sure the component is expanded 54 | } 55 | await this.object.storiesTreeLocator.locator(`#${id}`).click(); 56 | await this.object.assert.activePageIdEquals(id); 57 | } 58 | } 59 | 60 | class Waits { 61 | constructor(private object: AppObject) {} 62 | } 63 | 64 | export class AppObject { 65 | assert = new Assertions(this); 66 | 67 | action = new Actions(this); 68 | 69 | waitUntil = new Waits(this); 70 | 71 | activeStoryPage = new StoryPageObject(this.page); 72 | 73 | activeDocsPage = new DocsPageObject(this.page); 74 | 75 | constructor(public page: Page) {} 76 | 77 | async openDefaultPage() { 78 | const STORYBOOK_URL = `http://localhost:${STORYBOOK_PORT}/?path=/story/stories-dev--enabled`; 79 | 80 | try { 81 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 82 | } catch { 83 | // sometimes goto times out, so try again 84 | 85 | console.warn("page.goto timed out, trying again"); 86 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 87 | } 88 | 89 | await this.activeStoryPage.waitUntil.previewIframeLoaded(); 90 | } 91 | 92 | get storiesTreeLocator() { 93 | return this.page.locator("#storybook-explorer-tree"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/tests/objects/AppObject.ts: -------------------------------------------------------------------------------- 1 | import type {Page} from "@playwright/test"; 2 | import {expect} from "@playwright/test"; 3 | import {STORYBOOK_PORT} from "../../utils/constants"; 4 | import {DocsPageObject} from "./DocsPageObject"; 5 | import {StoryPageObject} from "./StoryPageObject"; 6 | 7 | class Assertions { 8 | constructor(private object: AppObject) {} 9 | 10 | /** 11 | * 12 | * @param expectedStoryId Can be story or Docs id 13 | */ 14 | async activePageIdEquals(expectedStoryId: string) { 15 | const actualId = await this.object.storiesTreeLocator.getAttribute("data-highlighted-item-id"); 16 | expect(actualId, {message: "active story id"}).toEqual(expectedStoryId); 17 | } 18 | } 19 | 20 | class Actions { 21 | constructor(private object: AppObject) {} 22 | 23 | /** 24 | * 25 | * @param id Story id, e.g. "stories-dev--enabled" 26 | * 27 | * @note This is the ID shown in the URL when you click on a story in the Storybook UI e.g. 28 | * `http://localhost:${STORYBOOK_V9_PORT}/?path=/story/stories-dev--enabled` 29 | */ 30 | async openStoriesTreeItemById(type: "story" | "docs", id: `stories-${string}--${string}`) { 31 | await this.clickStoryTreeItemById(id); 32 | 33 | // wait until loaded 34 | switch (type) { 35 | case "story": 36 | return this.object.activeStoryPage.waitUntil.previewIframeLoaded(); 37 | case "docs": 38 | return this.object.activeDocsPage.waitUntil.previewIframeLoaded(); 39 | default: 40 | throw Error(`Invalid tree item type: ${type}`); 41 | } 42 | } 43 | 44 | private async clickStoryTreeItemById(id: string) { 45 | if (!id.includes("--")) { 46 | throw new Error( 47 | `Invalid story id, ${id}, it should include "--" to separate the component and story id`, 48 | ); 49 | } 50 | const componentId = id.split("--")[0]; 51 | const storyIsVisible = await this.object.storiesTreeLocator.locator(`#${id}`).isVisible(); 52 | if (!storyIsVisible) { 53 | await this.object.storiesTreeLocator.locator(`#${componentId}`).click(); // make sure the component is expanded 54 | } 55 | await this.object.storiesTreeLocator.locator(`#${id}`).click(); 56 | await this.object.assert.activePageIdEquals(id); 57 | } 58 | } 59 | 60 | class Waits { 61 | constructor(private object: AppObject) {} 62 | } 63 | 64 | export class AppObject { 65 | assert = new Assertions(this); 66 | 67 | action = new Actions(this); 68 | 69 | waitUntil = new Waits(this); 70 | 71 | activeStoryPage = new StoryPageObject(this.page); 72 | 73 | activeDocsPage = new DocsPageObject(this.page); 74 | 75 | constructor(public page: Page) {} 76 | 77 | async openDefaultPage() { 78 | const STORYBOOK_URL = `http://localhost:${STORYBOOK_PORT}/?path=/story/stories-dev--enabled`; 79 | 80 | try { 81 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 82 | } catch { 83 | // sometimes goto times out, so try again 84 | 85 | console.warn("page.goto timed out, trying again"); 86 | await this.page.goto(STORYBOOK_URL, {timeout: 5000}); 87 | } 88 | 89 | await this.activeStoryPage.waitUntil.previewIframeLoaded(); 90 | } 91 | 92 | get storiesTreeLocator() { 93 | return this.page.locator("#storybook-explorer-tree"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/WithTypedProps.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows default controls when initial values are not defined", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withtypedprops--default-enabled", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.argsTable.assert.controlsMatch({ 22 | someString: { 23 | type: "set-value-button", 24 | valueType: "string", 25 | }, 26 | someObject: { 27 | type: "set-value-button", 28 | valueType: "object", 29 | }, 30 | someArray: { 31 | type: "set-value-button", 32 | valueType: "object", 33 | }, 34 | }); 35 | await storyPage.assert.actualConfigMatches({}); 36 | }); 37 | 38 | test("shows deep controls when initial values are defined", async ({page}) => { 39 | const storybookPage = new AppObject(page); 40 | await storybookPage.action.openStoriesTreeItemById("story", "stories-withtypedprops--with-args"); 41 | 42 | const storyPage = storybookPage.activeStoryPage; 43 | await storyPage.argsTable.assert.controlsMatch({ 44 | someString: { 45 | type: "set-value-button", 46 | valueType: "string", 47 | }, // still included 48 | "someObject.anyString": "anyString", 49 | "someObject.enumString": "value2", 50 | someArray: { 51 | type: "json", 52 | valueText: '[0 : "string1"1 : "string2"]', 53 | }, 54 | }); 55 | 56 | await storyPage.assert.actualConfigMatches({ 57 | someArray: ["string1", "string2"], 58 | someObject: { 59 | anyString: "anyString", 60 | enumString: "value2", 61 | }, 62 | }); 63 | }); 64 | 65 | test("supports customising controls with initial values", async ({page}) => { 66 | const storybookPage = new AppObject(page); 67 | await storybookPage.action.openStoriesTreeItemById( 68 | "story", 69 | "stories-withtypedprops--with-custom-controls", 70 | ); 71 | 72 | const storyPage = storybookPage.activeStoryPage; 73 | await storyPage.argsTable.assert.controlsMatch({ 74 | someString: { 75 | type: "radio", 76 | options: ["string1", "string2", "string3"], 77 | value: null, 78 | }, 79 | "someObject.anyString": "anyString", 80 | "someObject.enumString": { 81 | type: "radio", 82 | options: ["value1", "value2", "value3"], 83 | value: "value2", 84 | }, 85 | someArray: { 86 | type: "json", 87 | valueText: '[0 : "string1"1 : "string2"]', 88 | }, 89 | }); 90 | 91 | // initial value not affected by custom controls 92 | await storyPage.assert.actualConfigMatches({ 93 | someArray: ["string1", "string2"], 94 | someObject: { 95 | anyString: "anyString", 96 | enumString: "value2", 97 | }, 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/WithTypedProps.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows default controls when initial values are not defined", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withtypedprops--default-enabled", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.argsTable.assert.controlsMatch({ 22 | someString: { 23 | type: "set-value-button", 24 | valueType: "string", 25 | }, 26 | someObject: { 27 | type: "set-value-button", 28 | valueType: "object", 29 | }, 30 | someArray: { 31 | type: "set-value-button", 32 | valueType: "object", 33 | }, 34 | }); 35 | await storyPage.assert.actualConfigMatches({}); 36 | }); 37 | 38 | test("shows deep controls when initial values are defined", async ({page}) => { 39 | const storybookPage = new AppObject(page); 40 | await storybookPage.action.openStoriesTreeItemById("story", "stories-withtypedprops--with-args"); 41 | 42 | const storyPage = storybookPage.activeStoryPage; 43 | await storyPage.argsTable.assert.controlsMatch({ 44 | someString: { 45 | type: "set-value-button", 46 | valueType: "string", 47 | }, // still included 48 | "someObject.anyString": "anyString", 49 | "someObject.enumString": "value2", 50 | someArray: { 51 | type: "json", 52 | valueText: '[0 : "string1"1 : "string2"]', 53 | }, 54 | }); 55 | 56 | await storyPage.assert.actualConfigMatches({ 57 | someArray: ["string1", "string2"], 58 | someObject: { 59 | anyString: "anyString", 60 | enumString: "value2", 61 | }, 62 | }); 63 | }); 64 | 65 | test("supports customising controls with initial values", async ({page}) => { 66 | const storybookPage = new AppObject(page); 67 | await storybookPage.action.openStoriesTreeItemById( 68 | "story", 69 | "stories-withtypedprops--with-custom-controls", 70 | ); 71 | 72 | const storyPage = storybookPage.activeStoryPage; 73 | await storyPage.argsTable.assert.controlsMatch({ 74 | someString: { 75 | type: "radio", 76 | options: ["string1", "string2", "string3"], 77 | value: null, 78 | }, 79 | "someObject.anyString": "anyString", 80 | "someObject.enumString": { 81 | type: "radio", 82 | options: ["value1", "value2", "value3"], 83 | value: "value2", 84 | }, 85 | someArray: { 86 | type: "json", 87 | valueText: '[0 : "string1"1 : "string2"]', 88 | }, 89 | }); 90 | 91 | // initial value not affected by custom controls 92 | await storyPage.assert.actualConfigMatches({ 93 | someArray: ["string1", "string2"], 94 | someObject: { 95 | anyString: "anyString", 96 | enumString: "value2", 97 | }, 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/WithTypedProps.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@playwright/test"; 2 | import {AppObject} from "../tests/objects/AppObject"; 3 | import {assertStorybookIsRunning} from "../utils"; 4 | import {TEST_TIMEOUT_MS} from "../utils/constants"; 5 | 6 | test.beforeAll(assertStorybookIsRunning); 7 | 8 | test.beforeEach(async ({page}) => { 9 | test.setTimeout(TEST_TIMEOUT_MS); 10 | await new AppObject(page).openDefaultPage(); 11 | }); 12 | 13 | test("shows default controls when initial values are not defined", async ({page}) => { 14 | const storybookPage = new AppObject(page); 15 | await storybookPage.action.openStoriesTreeItemById( 16 | "story", 17 | "stories-withtypedprops--default-enabled", 18 | ); 19 | 20 | const storyPage = storybookPage.activeStoryPage; 21 | await storyPage.argsTable.assert.controlsMatch({ 22 | someString: { 23 | type: "set-value-button", 24 | valueType: "string", 25 | }, 26 | someObject: { 27 | type: "set-value-button", 28 | valueType: "object", 29 | }, 30 | someArray: { 31 | type: "set-value-button", 32 | valueType: "object", 33 | }, 34 | }); 35 | await storyPage.assert.actualConfigMatches({}); 36 | }); 37 | 38 | test("shows deep controls when initial values are defined", async ({page}) => { 39 | const storybookPage = new AppObject(page); 40 | await storybookPage.action.openStoriesTreeItemById("story", "stories-withtypedprops--with-args"); 41 | 42 | const storyPage = storybookPage.activeStoryPage; 43 | await storyPage.argsTable.assert.controlsMatch({ 44 | someString: { 45 | type: "set-value-button", 46 | valueType: "string", 47 | }, // still included 48 | "someObject.anyString": "anyString", 49 | "someObject.enumString": "value2", 50 | someArray: { 51 | type: "json", 52 | valueText: '[0 : "string1"1 : "string2"]', 53 | }, 54 | }); 55 | 56 | await storyPage.assert.actualConfigMatches({ 57 | someArray: ["string1", "string2"], 58 | someObject: { 59 | anyString: "anyString", 60 | enumString: "value2", 61 | }, 62 | }); 63 | }); 64 | 65 | test("supports customising controls with initial values", async ({page}) => { 66 | const storybookPage = new AppObject(page); 67 | await storybookPage.action.openStoriesTreeItemById( 68 | "story", 69 | "stories-withtypedprops--with-custom-controls", 70 | ); 71 | 72 | const storyPage = storybookPage.activeStoryPage; 73 | await storyPage.argsTable.assert.controlsMatch({ 74 | someString: { 75 | type: "radio", 76 | options: ["string1", "string2", "string3"], 77 | value: null, 78 | }, 79 | "someObject.anyString": "anyString", 80 | "someObject.enumString": { 81 | type: "radio", 82 | options: ["value1", "value2", "value3"], 83 | value: "value2", 84 | }, 85 | someArray: { 86 | type: "json", 87 | valueText: '[0 : "string1"1 : "string2"]', 88 | }, 89 | }); 90 | 91 | // initial value not affected by custom controls 92 | await storyPage.assert.actualConfigMatches({ 93 | someArray: ["string1", "string2"], 94 | someObject: { 95 | anyString: "anyString", 96 | enumString: "value2", 97 | }, 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/stories/Dev.stories.ts: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Dev from "./Dev"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta = { 8 | component: Dev, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | export const Enabled: Story = { 22 | args: createNestedObject(), 23 | }; 24 | 25 | export const Disabled: Story = { 26 | args: createNestedObject(), 27 | parameters: { 28 | deepControls: { 29 | enabled: false, 30 | }, 31 | }, 32 | }; 33 | 34 | export const WithCustomControls: Story = { 35 | args: { 36 | someObject: { 37 | anyString: "anyString", 38 | enumString: "enumString", 39 | }, 40 | }, 41 | argTypes: { 42 | "someObject.enumString": { 43 | control: "radio", 44 | options: ["value1", "value2", "value3"], 45 | }, 46 | }, 47 | }; 48 | 49 | export const RawObject: Story = { 50 | args: { 51 | someObject: { 52 | anyString: "anyString", 53 | enumString: "enumString", 54 | }, 55 | }, 56 | parameters: { 57 | deepControls: { 58 | enabled: false, 59 | }, 60 | }, 61 | }; 62 | 63 | // NOTE: this doesn't include BigInt as Storybook cant serialise this 64 | function createNestedObject() { 65 | return { 66 | bool: true, 67 | string: "string1234", 68 | number: 1234, 69 | nested: { 70 | bool: false, 71 | string: "string2", 72 | number: 2, 73 | nestedWithoutPrototype: Object.assign(Object.create(null), { 74 | bool: true, 75 | string: "string3", 76 | element: document.createElement("span"), 77 | }), 78 | nullValue: null, 79 | element: document.createElement("div"), 80 | func: () => {}, 81 | nested: { 82 | bool: true, 83 | string: "string3", 84 | number: -3, 85 | nullValue: null, 86 | infinity: Infinity, 87 | NaNValue: NaN, 88 | symbol: Symbol("symbol"), 89 | classRef: class Foo {}, 90 | numberArray: [1, 2, 3], 91 | complexArray: [ 92 | { 93 | bool: true, 94 | string: "string3", 95 | number: -3, 96 | }, 97 | document.createElement("div"), 98 | null, 99 | Symbol("symbol"), 100 | class Bar {}, 101 | function () {}, 102 | ], 103 | }, 104 | }, 105 | }; 106 | } 107 | 108 | export const WithControlMatchers: TypeWithDeepControls = { 109 | parameters: { 110 | controls: { 111 | // see https://storybook.js.org/docs/essentials/controls#custom-control-type-matchers 112 | matchers: { 113 | color: /color/i, 114 | }, 115 | }, 116 | }, 117 | args: { 118 | color: { 119 | color: "#f00", 120 | description: "Very red", 121 | }, 122 | }, 123 | }; 124 | -------------------------------------------------------------------------------- /packages/addon/src/utils/general.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from "vitest"; 2 | import {getProperty, setProperty} from "./general"; 3 | 4 | describe("general utils", () => { 5 | describe("setProperty", () => { 6 | it("can add nested properties to objects", () => { 7 | const obj = {}; 8 | setProperty(obj, "a.b.c", 1); 9 | expect(obj).toEqual({a: {b: {c: 1}}}); 10 | }); 11 | 12 | it("can overwrite nested object properties", () => { 13 | const obj = {a: {b: {c: 1}}}; 14 | setProperty(obj, "a.b.c", 2); 15 | expect(obj).toEqual({a: {b: {c: 2}}}); 16 | }); 17 | 18 | it("can overwrite nested array object item properties", () => { 19 | const obj = {a: [{b: 1}, {b: 2}, {b: 3}]}; 20 | setProperty(obj, "a.1.b", 4); 21 | expect(obj).toEqual({a: [{b: 1}, {b: 4}, {b: 3}]}); 22 | }); 23 | 24 | it("can overwrite non-object array item", () => { 25 | const obj = {a: [1, 2, 3]}; 26 | setProperty(obj, "a.1", 4); 27 | expect(obj).toEqual({a: [1, 4, 3]}); 28 | }); 29 | 30 | // ie mainly testing it doesnt throw 31 | describe("non object values handling", () => { 32 | it("number", () => { 33 | const obj: any = 1; 34 | setProperty(obj, "prop", 1); 35 | expect(obj).toBe(1); 36 | }); 37 | 38 | it("string", () => { 39 | const obj: any = "string"; 40 | setProperty(obj, "prop", 1); 41 | expect(obj).toBe("string"); 42 | }); 43 | 44 | it("boolean", () => { 45 | const obj: any = true; 46 | setProperty(obj, "prop", 1); 47 | expect(obj).toBe(true); 48 | }); 49 | 50 | it("null", () => { 51 | const obj: any = null; 52 | setProperty(obj, "prop", 1); 53 | expect(obj).toBe(null); 54 | }); 55 | 56 | it("undefined", () => { 57 | const obj: any = undefined; 58 | setProperty(obj, "prop", 1); 59 | expect(obj).toBe(undefined); 60 | }); 61 | }); 62 | }); 63 | 64 | describe("getProperty", () => { 65 | it("can get properties of nested objects", () => { 66 | const obj = {a: {b: {c: 1}}}; 67 | expect(getProperty(obj, "a.b.c")).toBe(1); 68 | }); 69 | 70 | it("can get properties of items in arrays", () => { 71 | const obj = {a: [{b: 1}, {b: 2}, {b: 3}]}; 72 | expect(getProperty(obj, "a.1.b")).toBe(2); 73 | }); 74 | 75 | describe("cant get properties of non objects", () => { 76 | // property that is common to all values 77 | const GLOBALLY_COMMON_PROPERTY = "toString"; 78 | 79 | it("number", () => { 80 | expect((1)[GLOBALLY_COMMON_PROPERTY]).toBeDefined(); 81 | expect(getProperty(1, GLOBALLY_COMMON_PROPERTY)).toBeUndefined(); 82 | }); 83 | 84 | it("string", () => { 85 | expect("string"[GLOBALLY_COMMON_PROPERTY]).toBeDefined(); 86 | expect(getProperty("string", GLOBALLY_COMMON_PROPERTY)).toBeUndefined(); 87 | }); 88 | 89 | it("boolean", () => { 90 | expect(true[GLOBALLY_COMMON_PROPERTY]).toBeDefined(); 91 | expect(getProperty(true, GLOBALLY_COMMON_PROPERTY)).toBeUndefined(); 92 | }); 93 | 94 | it("null", () => { 95 | expect(getProperty(null, GLOBALLY_COMMON_PROPERTY)).toBeUndefined(); 96 | }); 97 | 98 | it("undefined", () => { 99 | expect(getProperty(undefined, GLOBALLY_COMMON_PROPERTY)).toBeUndefined(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/example-prod/src/stories/Dev.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Dev from "./Dev"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | component: Dev, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | 20 | type Story = TypeWithDeepControls>; 21 | 22 | export const Enabled: Story = { 23 | args: createNestedObject(), 24 | }; 25 | 26 | export const Disabled: Story = { 27 | args: createNestedObject(), 28 | parameters: { 29 | deepControls: { 30 | enabled: false, 31 | }, 32 | }, 33 | }; 34 | 35 | export const WithCustomControls: Story = { 36 | args: { 37 | someObject: { 38 | anyString: "anyString", 39 | enumString: "value2", 40 | }, 41 | }, 42 | argTypes: { 43 | "someObject.enumString": { 44 | control: "radio", 45 | options: ["value1", "value2", "value3"], 46 | }, 47 | }, 48 | }; 49 | 50 | export const WithCustomControlsForNonExistingProperty: Story = { 51 | args: { 52 | someObject: { 53 | anyString: "anyString", 54 | enumString: "value2", 55 | }, 56 | }, 57 | argTypes: { 58 | "someObject.unknown": { 59 | control: "radio", 60 | options: ["value1", "value2", "value3"], 61 | }, 62 | }, 63 | }; 64 | 65 | export const DisabledWithSimpleObject: Story = { 66 | args: { 67 | someObject: { 68 | anyString: "anyString", 69 | enumString: "value2", 70 | }, 71 | }, 72 | parameters: { 73 | deepControls: { 74 | enabled: false, 75 | }, 76 | }, 77 | }; 78 | 79 | // NOTE: this doesn't include BigInt as Storybook cant serialise this 80 | function createNestedObject() { 81 | return { 82 | bool: true, 83 | string: "string1234", 84 | number: 1234, 85 | jsx:
, 86 | nested: { 87 | jsx:
, 88 | bool: false, 89 | string: "string2", 90 | number: 2, 91 | nestedWithoutPrototype: Object.assign(Object.create(null), { 92 | bool: true, 93 | string: "string3", 94 | element: document.createElement("span"), 95 | }), 96 | nullValue: null, 97 | element: document.createElement("div"), 98 | func: () => {}, 99 | nested: { 100 | bool: true, 101 | string: "string3", 102 | number: -3, 103 | nullValue: null, 104 | infinity: Infinity, 105 | NaNValue: NaN, 106 | symbol: Symbol("symbol"), 107 | classRef: class Foo {}, 108 | numberArray: [1, 2, 3], 109 | complexArray: [ 110 | { 111 | bool: true, 112 | string: "string3", 113 | number: -3, 114 | }, 115 | document.createElement("div"), 116 | null, 117 | Symbol("symbol"), 118 | class Bar {}, 119 | function () {}, 120 | ], 121 | }, 122 | }, 123 | }; 124 | } 125 | 126 | export const WithControlMatchers: Story = { 127 | parameters: { 128 | controls: { 129 | // see https://storybook.js.org/docs/essentials/controls#custom-control-type-matchers 130 | matchers: { 131 | color: /color/i, 132 | }, 133 | }, 134 | }, 135 | args: { 136 | color: { 137 | color: "#f00", 138 | description: "Very red", 139 | }, 140 | }, 141 | }; 142 | 143 | export const WithEmptyInitialArgs: Story = { 144 | args: { 145 | emptyObj: {}, 146 | emptyArray: [], 147 | }, 148 | }; 149 | 150 | export const WithOverriddenObjectArg: Story = { 151 | args: { 152 | someObject: { 153 | obj1: { 154 | foo1: "foo1", 155 | bar1: "bar1", 156 | }, 157 | obj2WithArgType: { 158 | foo2: "foo2", 159 | bar2: "bar2", 160 | }, 161 | }, 162 | }, 163 | argTypes: { 164 | // obj1 should be deep controlled 165 | // obj2 should be shown with same value in json control 166 | "someObject.obj2WithArgType": {control: "object"}, 167 | }, 168 | }; 169 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/stories/Dev.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Dev from "./Dev"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | component: Dev, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | 20 | type Story = TypeWithDeepControls>; 21 | 22 | export const Enabled: Story = { 23 | args: createNestedObject(), 24 | }; 25 | 26 | export const Disabled: Story = { 27 | args: createNestedObject(), 28 | parameters: { 29 | deepControls: { 30 | enabled: false, 31 | }, 32 | }, 33 | }; 34 | 35 | export const WithCustomControls: Story = { 36 | args: { 37 | someObject: { 38 | anyString: "anyString", 39 | enumString: "value2", 40 | }, 41 | }, 42 | argTypes: { 43 | "someObject.enumString": { 44 | control: "radio", 45 | options: ["value1", "value2", "value3"], 46 | }, 47 | }, 48 | }; 49 | 50 | export const WithCustomControlsForNonExistingProperty: Story = { 51 | args: { 52 | someObject: { 53 | anyString: "anyString", 54 | enumString: "value2", 55 | }, 56 | }, 57 | argTypes: { 58 | "someObject.unknown": { 59 | control: "radio", 60 | options: ["value1", "value2", "value3"], 61 | }, 62 | }, 63 | }; 64 | 65 | export const DisabledWithSimpleObject: Story = { 66 | args: { 67 | someObject: { 68 | anyString: "anyString", 69 | enumString: "value2", 70 | }, 71 | }, 72 | parameters: { 73 | deepControls: { 74 | enabled: false, 75 | }, 76 | }, 77 | }; 78 | 79 | // NOTE: this doesn't include BigInt as Storybook cant serialise this 80 | function createNestedObject() { 81 | return { 82 | bool: true, 83 | string: "string1234", 84 | number: 1234, 85 | jsx:
, 86 | nested: { 87 | jsx:
, 88 | bool: false, 89 | string: "string2", 90 | number: 2, 91 | nestedWithoutPrototype: Object.assign(Object.create(null), { 92 | bool: true, 93 | string: "string3", 94 | element: document.createElement("span"), 95 | }), 96 | nullValue: null, 97 | element: document.createElement("div"), 98 | func: () => {}, 99 | nested: { 100 | bool: true, 101 | string: "string3", 102 | number: -3, 103 | nullValue: null, 104 | infinity: Infinity, 105 | NaNValue: NaN, 106 | symbol: Symbol("symbol"), 107 | classRef: class Foo {}, 108 | numberArray: [1, 2, 3], 109 | complexArray: [ 110 | { 111 | bool: true, 112 | string: "string3", 113 | number: -3, 114 | }, 115 | document.createElement("div"), 116 | null, 117 | Symbol("symbol"), 118 | class Bar {}, 119 | function () {}, 120 | ], 121 | }, 122 | }, 123 | }; 124 | } 125 | 126 | export const WithControlMatchers: Story = { 127 | parameters: { 128 | controls: { 129 | // see https://storybook.js.org/docs/essentials/controls#custom-control-type-matchers 130 | matchers: { 131 | color: /color/i, 132 | }, 133 | }, 134 | }, 135 | args: { 136 | color: { 137 | color: "#f00", 138 | description: "Very red", 139 | }, 140 | }, 141 | }; 142 | 143 | export const WithEmptyInitialArgs: Story = { 144 | args: { 145 | emptyObj: {}, 146 | emptyArray: [], 147 | }, 148 | }; 149 | 150 | export const WithOverriddenObjectArg: Story = { 151 | args: { 152 | someObject: { 153 | obj1: { 154 | foo1: "foo1", 155 | bar1: "bar1", 156 | }, 157 | obj2WithArgType: { 158 | foo2: "foo2", 159 | bar2: "bar2", 160 | }, 161 | }, 162 | }, 163 | argTypes: { 164 | // obj1 should be deep controlled 165 | // obj2 should be shown with same value in json control 166 | "someObject.obj2WithArgType": {control: "object"}, 167 | }, 168 | }; 169 | -------------------------------------------------------------------------------- /packages/example-v9-generic/src/stories/Dev.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Dev from "./Dev"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | component: Dev, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | 20 | type Story = TypeWithDeepControls>; 21 | 22 | export const Enabled: Story = { 23 | args: createNestedObject(), 24 | }; 25 | 26 | export const Disabled: Story = { 27 | args: createNestedObject(), 28 | parameters: { 29 | deepControls: { 30 | enabled: false, 31 | }, 32 | }, 33 | }; 34 | 35 | export const WithCustomControls: Story = { 36 | args: { 37 | someObject: { 38 | anyString: "anyString", 39 | enumString: "value2", 40 | }, 41 | }, 42 | argTypes: { 43 | "someObject.enumString": { 44 | control: "radio", 45 | options: ["value1", "value2", "value3"], 46 | }, 47 | }, 48 | }; 49 | 50 | export const WithCustomControlsForNonExistingProperty: Story = { 51 | args: { 52 | someObject: { 53 | anyString: "anyString", 54 | enumString: "value2", 55 | }, 56 | }, 57 | argTypes: { 58 | "someObject.unknown": { 59 | control: "radio", 60 | options: ["value1", "value2", "value3"], 61 | }, 62 | }, 63 | }; 64 | 65 | export const DisabledWithSimpleObject: Story = { 66 | args: { 67 | someObject: { 68 | anyString: "anyString", 69 | enumString: "value2", 70 | }, 71 | }, 72 | parameters: { 73 | deepControls: { 74 | enabled: false, 75 | }, 76 | }, 77 | }; 78 | 79 | // NOTE: this doesn't include BigInt as Storybook cant serialise this 80 | function createNestedObject() { 81 | return { 82 | bool: true, 83 | string: "string1234", 84 | number: 1234, 85 | jsx:
, 86 | nested: { 87 | jsx:
, 88 | bool: false, 89 | string: "string2", 90 | number: 2, 91 | nestedWithoutPrototype: Object.assign(Object.create(null), { 92 | bool: true, 93 | string: "string3", 94 | element: document.createElement("span"), 95 | }), 96 | nullValue: null, 97 | element: document.createElement("div"), 98 | func: () => {}, 99 | nested: { 100 | bool: true, 101 | string: "string3", 102 | number: -3, 103 | nullValue: null, 104 | infinity: Infinity, 105 | NaNValue: NaN, 106 | symbol: Symbol("symbol"), 107 | classRef: class Foo {}, 108 | numberArray: [1, 2, 3], 109 | complexArray: [ 110 | { 111 | bool: true, 112 | string: "string3", 113 | number: -3, 114 | }, 115 | document.createElement("div"), 116 | null, 117 | Symbol("symbol"), 118 | class Bar {}, 119 | function () {}, 120 | ], 121 | }, 122 | }, 123 | }; 124 | } 125 | 126 | export const WithControlMatchers: Story = { 127 | parameters: { 128 | controls: { 129 | // see https://storybook.js.org/docs/essentials/controls#custom-control-type-matchers 130 | matchers: { 131 | color: /color/i, 132 | }, 133 | }, 134 | }, 135 | args: { 136 | color: { 137 | color: "#f00", 138 | description: "Very red", 139 | }, 140 | }, 141 | }; 142 | 143 | export const WithEmptyInitialArgs: Story = { 144 | args: { 145 | emptyObj: {}, 146 | emptyArray: [], 147 | }, 148 | }; 149 | 150 | export const WithOverriddenObjectArg: Story = { 151 | args: { 152 | someObject: { 153 | obj1: { 154 | foo1: "foo1", 155 | bar1: "bar1", 156 | }, 157 | obj2WithArgType: { 158 | foo2: "foo2", 159 | bar2: "bar2", 160 | }, 161 | }, 162 | }, 163 | argTypes: { 164 | // obj1 should be deep controlled 165 | // obj2 should be shown with same value in json control 166 | "someObject.obj2WithArgType": {control: "object"}, 167 | }, 168 | }; 169 | -------------------------------------------------------------------------------- /packages/example-v8-generic/src/stories/Dev.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from "@storybook/react"; 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import Dev from "./Dev"; 4 | 5 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | component: Dev, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: "centered", 12 | deepControls: { 13 | enabled: true, 14 | }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | 20 | type Story = TypeWithDeepControls>; 21 | 22 | export const Enabled: Story = { 23 | args: createNestedObject(), 24 | }; 25 | 26 | export const Disabled: Story = { 27 | args: createNestedObject(), 28 | parameters: { 29 | deepControls: { 30 | enabled: false, 31 | }, 32 | }, 33 | }; 34 | 35 | export const WithCustomControls: Story = { 36 | args: { 37 | someObject: { 38 | anyString: "anyString", 39 | enumString: "value2", 40 | }, 41 | }, 42 | argTypes: { 43 | "someObject.enumString": { 44 | control: "radio", 45 | options: ["value1", "value2", "value3"], 46 | }, 47 | }, 48 | }; 49 | 50 | export const WithCustomControlsForNonExistingProperty: Story = { 51 | args: { 52 | someObject: { 53 | anyString: "anyString", 54 | enumString: "value2", 55 | }, 56 | }, 57 | argTypes: { 58 | "someObject.unknown": { 59 | control: "radio", 60 | options: ["value1", "value2", "value3"], 61 | }, 62 | }, 63 | }; 64 | 65 | export const DisabledWithSimpleObject: Story = { 66 | args: { 67 | someObject: { 68 | anyString: "anyString", 69 | enumString: "enumString", 70 | }, 71 | }, 72 | parameters: { 73 | deepControls: { 74 | enabled: false, 75 | }, 76 | }, 77 | }; 78 | 79 | // NOTE: this doesn't include BigInt as Storybook cant serialise this 80 | function createNestedObject() { 81 | return { 82 | bool: true, 83 | string: "string1234", 84 | number: 1234, 85 | jsx:
, 86 | nested: { 87 | jsx:
, 88 | bool: false, 89 | string: "string2", 90 | number: 2, 91 | nestedWithoutPrototype: Object.assign(Object.create(null), { 92 | bool: true, 93 | string: "string3", 94 | element: document.createElement("span"), 95 | }), 96 | nullValue: null, 97 | element: document.createElement("div"), 98 | func: () => {}, 99 | nested: { 100 | bool: true, 101 | string: "string3", 102 | number: -3, 103 | nullValue: null, 104 | infinity: Infinity, 105 | NaNValue: NaN, 106 | symbol: Symbol("symbol"), 107 | classRef: class Foo {}, 108 | numberArray: [1, 2, 3], 109 | complexArray: [ 110 | { 111 | bool: true, 112 | string: "string3", 113 | number: -3, 114 | }, 115 | document.createElement("div"), 116 | null, 117 | Symbol("symbol"), 118 | class Bar {}, 119 | function () {}, 120 | ], 121 | }, 122 | }, 123 | }; 124 | } 125 | 126 | export const WithControlMatchers: Story = { 127 | parameters: { 128 | controls: { 129 | // see https://storybook.js.org/docs/essentials/controls#custom-control-type-matchers 130 | matchers: { 131 | color: /color/i, 132 | }, 133 | }, 134 | }, 135 | args: { 136 | color: { 137 | color: "#f00", 138 | description: "Very red", 139 | }, 140 | }, 141 | }; 142 | 143 | export const WithEmptyInitialArgs: Story = { 144 | args: { 145 | emptyObj: {}, 146 | emptyArray: [], 147 | }, 148 | }; 149 | 150 | export const WithOverriddenObjectArg: Story = { 151 | args: { 152 | someObject: { 153 | obj1: { 154 | foo1: "foo1", 155 | bar1: "bar1", 156 | }, 157 | obj2WithArgType: { 158 | foo2: "foo2", 159 | bar2: "bar2", 160 | }, 161 | }, 162 | }, 163 | argTypes: { 164 | // obj1 should be deep controlled 165 | // obj2 should be shown with same value in json control 166 | "someObject.obj2WithArgType": {control: "object"}, 167 | }, 168 | }; 169 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/tests/utils/StorybookPage.ts: -------------------------------------------------------------------------------- 1 | import type {Page} from "@playwright/test"; 2 | import {expect} from "@playwright/test"; 3 | import {setTimeout} from "timers/promises"; 4 | import {STORYBOOK_V7_PORT} from "./constants"; 5 | 6 | type ControlExpectation = 7 | | string 8 | | number 9 | | boolean 10 | | undefined 11 | | unknown[] 12 | | { 13 | type: "radio"; 14 | options: string[]; 15 | } 16 | | { 17 | type: "color"; 18 | value: string; 19 | }; 20 | 21 | class Assertions { 22 | constructor(private object: StorybookPageObject) {} 23 | 24 | async actualConfigMatches(expectedConfig: Record) { 25 | await setTimeout(1000); // wait for changes to be applied, reduces flakiness 26 | const actualConfigText = await this.object.previewIframeLocator 27 | .locator("#actual-config-json") 28 | .innerText(); 29 | expect(JSON.parse(actualConfigText), "output config equals").toEqual(expectedConfig); 30 | } 31 | 32 | /** 33 | * Map of control names to their expected values. 34 | * - Primitive values are asserted to be equal to the given value 35 | * - Arrays are asserted to just show an array control but the value is not asserted 36 | * 37 | * @remark `undefined` means the control exists but no value is set 38 | */ 39 | async controlsMatch(expectedControlsMap: Record) { 40 | // check controls count to make sure we are not missing any 41 | const actualControlsAddonTabTitle = await this.object.page 42 | .locator("#tabbutton-addon-controls") 43 | .textContent(); 44 | const expectedControlEntries = Object.entries(expectedControlsMap); 45 | expect(actualControlsAddonTabTitle?.trim(), "controls tab title equals").toEqual( 46 | `Controls${expectedControlEntries.length}`, 47 | ); 48 | 49 | // check control values 50 | for (const [controlName, expectedRawValue] of expectedControlEntries) { 51 | // handle unset controls 52 | if (expectedRawValue === undefined) { 53 | const setControlButton = this.object.getLocatorForSetControlButton(controlName); 54 | await expect(setControlButton, `control "${controlName}" exists`).toBeVisible(); 55 | continue; 56 | } 57 | 58 | const controlInput = this.object.getLocatorForControlInput(controlName); 59 | 60 | // handle arrays 61 | if (Array.isArray(expectedRawValue)) { 62 | const controlNameLocator = this.object.addonsPanelLocator.getByText(controlName, { 63 | exact: true, 64 | }); 65 | await expect(controlNameLocator, `control name "${controlName}" exists`).toBeVisible(); 66 | // cant assert these complex controls the best we can do is just say they don't exist as simple inputs 67 | await expect( 68 | controlInput, 69 | `simple input for control "${controlName}" does not exist`, 70 | ).not.toBeVisible(); 71 | continue; 72 | } 73 | 74 | if (typeof expectedRawValue === "object") { 75 | // handle radio controls 76 | if (expectedRawValue.type === "radio") { 77 | const actualOptions = await this.object.getOptionsForRadioControl(controlName); 78 | expect(actualOptions, `control "${controlName}" radio input options`).toEqual( 79 | expectedRawValue.options, 80 | ); 81 | continue; 82 | } 83 | 84 | // handle color inputs 85 | if (expectedRawValue.type === "color") { 86 | const actualValue = await this.object.getValueForColorInput(controlName); 87 | expect(actualValue, `control "${controlName}" color value`).toEqual( 88 | expectedRawValue.value, 89 | ); 90 | continue; 91 | } 92 | } 93 | 94 | // handle boolean toggles 95 | if (typeof expectedRawValue === "boolean") { 96 | expect(await controlInput.isChecked(), `control "${controlName}" is checked`).toEqual( 97 | expectedRawValue, 98 | ); 99 | continue; 100 | } 101 | 102 | // handle primitive values 103 | const expectedValue = getEquivalentValueForInput(expectedRawValue); 104 | await expect(controlInput, `control "${controlName}" value equals`).toHaveValue( 105 | expectedValue, 106 | ); 107 | } 108 | } 109 | 110 | async activeStoryIdEquals(expectedStoryId: string) { 111 | const actualId = await this.object.storiesTreeLocator.getAttribute("data-highlighted-item-id"); 112 | expect(actualId, {message: "active story id"}).toEqual(expectedStoryId); 113 | } 114 | } 115 | 116 | class Actions { 117 | constructor(private object: StorybookPageObject) {} 118 | 119 | /** 120 | * 121 | * @param id Story id, e.g. "stories-dev--enabled" 122 | */ 123 | async clickStoryById(id: `${string}--${string}`) { 124 | if (!id.includes("--")) { 125 | throw new Error( 126 | `Invalid story id, ${id}, it should include "--" to separate the component and story id`, 127 | ); 128 | } 129 | const componentId = id.split("--")[0]; 130 | const storyIsVisible = await this.object.storiesTreeLocator.locator(`#${id}`).isVisible(); 131 | if (!storyIsVisible) { 132 | await this.object.storiesTreeLocator.locator(`#${componentId}`).click(); // make sure the component is expanded 133 | } 134 | await this.object.storiesTreeLocator.locator(`#${id}`).click(); 135 | await this.object.assert.activeStoryIdEquals(id); 136 | } 137 | } 138 | 139 | function getEquivalentValueForInput(rawValue: unknown): string { 140 | switch (typeof rawValue) { 141 | case "number": { 142 | if (Number.isNaN(rawValue) || !Number.isFinite(rawValue)) { 143 | return ""; // shows as an empty number input 144 | } 145 | return String(rawValue); 146 | } 147 | 148 | default: { 149 | return String(rawValue); 150 | } 151 | } 152 | } 153 | 154 | export default class StorybookPageObject { 155 | private readonly PREVIEW_IFRAME_SELECTOR = `iframe[title="storybook-preview-iframe"]`; 156 | 157 | assert = new Assertions(this); 158 | 159 | action = new Actions(this); 160 | 161 | constructor(public page: Page) {} 162 | 163 | async openPage() { 164 | const STORYBOOK_URL = `http://localhost:${STORYBOOK_V7_PORT}/?path=/story/stories-dev--enabled`; 165 | await this.page.goto(STORYBOOK_URL); 166 | await this.page.waitForSelector(this.PREVIEW_IFRAME_SELECTOR, {state: "visible"}); 167 | } 168 | 169 | get previewIframeLocator() { 170 | return this.page.frameLocator(this.PREVIEW_IFRAME_SELECTOR); 171 | } 172 | 173 | get resetControlsButtonLocator() { 174 | return this.page.getByRole("button", {name: "Reset controls"}); 175 | } 176 | 177 | get addonsPanelLocator() { 178 | return this.page.locator("#storybook-panel-root"); 179 | } 180 | 181 | get storiesTreeLocator() { 182 | return this.page.locator("#storybook-explorer-tree"); 183 | } 184 | 185 | /** 186 | * @param controlName The name of the control as shown in the UI Controls panel in the "Name" column, e.g. "bool" 187 | */ 188 | getLocatorForControlInput(controlName: string) { 189 | return this.addonsPanelLocator.locator(`[id='control-${controlName}']`); 190 | } 191 | 192 | /** 193 | * When a control doesn't have a value a button is shown to set the value 194 | * 195 | * @param controlName The name of the control as shown in the UI Controls panel in the "Name" column, e.g. "bool" 196 | */ 197 | getLocatorForSetControlButton(controlName: string) { 198 | return this.addonsPanelLocator.locator(`button[id='set-${controlName}']`); 199 | } 200 | 201 | getOptionsForRadioControl(controlName: string) { 202 | return this.addonsPanelLocator.locator(`label[for^='control-${controlName}']`).allInnerTexts(); 203 | } 204 | 205 | getValueForColorInput(controlName: string) { 206 | return this.getLocatorForControlInput(controlName).inputValue(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /packages/example-v10-generic/src/tests/types-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import type {Meta, StoryObj} from "@storybook/react"; 4 | import type React from "react"; 5 | 6 | // NOTE: copy this file to other example packages to test types for different versions of Storybook 7 | 8 | // mocks just to structure tests 9 | function describe(name: string, fn: () => void): void {} 10 | function it(name: string, fn: () => void): void {} 11 | 12 | describe("Types", function () { 13 | describe("TypeWithDeepControls", function () { 14 | type Props = {bool: boolean; num: number}; 15 | type Cmp = React.ComponentType; 16 | type MetaType = TypeWithDeepControls>; 17 | type StoryType = TypeWithDeepControls>; 18 | 19 | it("story: works", function () { 20 | const _: StoryType = { 21 | args: {bool: true, num: 1}, 22 | argTypes: { 23 | bool: { 24 | type: "boolean", 25 | name: "name", 26 | description: "description", 27 | }, 28 | num: { 29 | type: {name: "number"}, 30 | name: "name", 31 | description: "description", 32 | }, 33 | "obj.bool": { 34 | type: "boolean", 35 | name: "name", 36 | description: "description", 37 | }, 38 | }, 39 | parameters: { 40 | deepControls: {enabled: true}, 41 | }, 42 | }; 43 | }); 44 | 45 | it("meta: works", function () { 46 | const _: MetaType = { 47 | args: {bool: true, num: 1}, 48 | argTypes: { 49 | bool: { 50 | type: "boolean", 51 | name: "name", 52 | description: "description", 53 | }, 54 | num: { 55 | type: {name: "number"}, 56 | name: "name", 57 | description: "description", 58 | }, 59 | "obj.bool": { 60 | type: "boolean", 61 | name: "name", 62 | description: "description", 63 | }, 64 | }, 65 | parameters: { 66 | deepControls: {enabled: true}, 67 | }, 68 | }; 69 | }); 70 | 71 | it("story: checks parameters types", function () { 72 | const _: StoryType = { 73 | parameters: { 74 | deepControls: { 75 | // @ts-expect-error - should be boolean 76 | enabled: "true", 77 | }, 78 | }, 79 | }; 80 | }); 81 | 82 | it("meta: checks parameters types", function () { 83 | const _: MetaType = { 84 | parameters: { 85 | deepControls: { 86 | // @ts-expect-error - should be boolean 87 | enabled: "true", 88 | }, 89 | }, 90 | }; 91 | }); 92 | 93 | it("story: checks args types", function () { 94 | const _: StoryType = { 95 | args: { 96 | // @ts-expect-error - should be boolean 97 | bool: "true", 98 | // @ts-expect-error - should be number 99 | num: true, 100 | // allows unknown args 101 | unknown: "foo", 102 | }, 103 | }; 104 | }); 105 | 106 | it("meta: checks args types", function () { 107 | const _: MetaType = { 108 | args: { 109 | // @ts-expect-error - should be boolean 110 | bool: "true", 111 | // @ts-expect-error - should be number 112 | num: true, 113 | // allows unknown args 114 | unknown: "foo", 115 | }, 116 | }; 117 | }); 118 | 119 | it("story: does not allow unknown argTypes without dot notation", function () { 120 | const _: StoryType = { 121 | argTypes: { 122 | // @ts-expect-error - unknown argTypes not allowed 123 | unknown: { 124 | control: "text", 125 | name: "name", 126 | description: "description", 127 | }, 128 | // arg types with dot notation allowed 129 | "obj.bool": { 130 | control: "boolean", 131 | name: "name", 132 | description: "description", 133 | }, 134 | }, 135 | }; 136 | }); 137 | 138 | it("meta: does not allow unknown argTypes without dot notation", function () { 139 | const _: MetaType = { 140 | argTypes: { 141 | // @ts-expect-error - unknown argTypes not allowed 142 | unknown: { 143 | control: "text", 144 | name: "name", 145 | description: "description", 146 | }, 147 | // arg types with dot notation allowed 148 | "obj.bool": { 149 | control: "boolean", 150 | name: "name", 151 | description: "description", 152 | }, 153 | }, 154 | }; 155 | }); 156 | 157 | it("story: checks argTypes types", function () { 158 | const _: StoryType = { 159 | argTypes: { 160 | bool: { 161 | // @ts-expect-error - unknown control type 162 | type: "unknown", 163 | // @ts-expect-error - should be string 164 | name: 1, 165 | // @ts-expect-error - should be string 166 | description: 1, 167 | }, 168 | num: { 169 | type: { 170 | // @ts-expect-error - unknown control type 171 | name: "unknown", 172 | }, 173 | // @ts-expect-error - should be string 174 | name: 1, 175 | // @ts-expect-error - should be string 176 | description: 1, 177 | }, 178 | "obj.bool": { 179 | // @ts-expect-error - unknown control type 180 | type: "unknown", 181 | // @ts-expect-error - should be string 182 | name: 1, 183 | // @ts-expect-error - should be string 184 | description: 1, 185 | }, 186 | "obj.num": { 187 | type: { 188 | // @ts-expect-error - unknown control type 189 | name: "unknown", 190 | }, 191 | // @ts-expect-error - should be string 192 | name: 1, 193 | // @ts-expect-error - should be string 194 | description: 1, 195 | }, 196 | "obj.nums": { 197 | // @ts-expect-error - missing value property 198 | type: { 199 | name: "array", 200 | }, 201 | // @ts-expect-error - should be string 202 | name: 1, 203 | // @ts-expect-error - should be string 204 | description: 1, 205 | }, 206 | }, 207 | }; 208 | }); 209 | 210 | it("meta: checks argTypes types", function () { 211 | const _: MetaType = { 212 | argTypes: { 213 | bool: { 214 | // @ts-expect-error - unknown control type 215 | type: "unknown", 216 | // @ts-expect-error - should be string 217 | name: 1, 218 | // @ts-expect-error - should be string 219 | description: 1, 220 | }, 221 | num: { 222 | type: { 223 | // @ts-expect-error - unknown control type 224 | name: "unknown", 225 | }, 226 | // @ts-expect-error - should be string 227 | name: 1, 228 | // @ts-expect-error - should be string 229 | description: 1, 230 | }, 231 | "obj.bool": { 232 | // @ts-expect-error - unknown control type 233 | type: "unknown", 234 | // @ts-expect-error - should be string 235 | name: 1, 236 | // @ts-expect-error - should be string 237 | description: 1, 238 | }, 239 | "obj.num": { 240 | type: { 241 | // @ts-expect-error - unknown control type 242 | name: "unknown", 243 | }, 244 | // @ts-expect-error - should be string 245 | name: 1, 246 | // @ts-expect-error - should be string 247 | description: 1, 248 | }, 249 | "obj.nums": { 250 | // @ts-expect-error - missing value property 251 | type: { 252 | name: "array", 253 | }, 254 | // @ts-expect-error - should be string 255 | name: 1, 256 | // @ts-expect-error - should be string 257 | description: 1, 258 | }, 259 | }, 260 | }; 261 | }); 262 | 263 | it("story: allows partial argTypes types", function () { 264 | const _: StoryType = { 265 | argTypes: { 266 | bool: {description: "description"}, 267 | num: {description: "description"}, 268 | "obj.bool": {description: "description"}, 269 | }, 270 | }; 271 | }); 272 | 273 | it("meta: allows partial argTypes types", function () { 274 | const _: MetaType = { 275 | argTypes: { 276 | bool: {description: "description"}, 277 | num: {description: "description"}, 278 | "obj.bool": {description: "description"}, 279 | }, 280 | }; 281 | }); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /packages/example-v7-webpack/src/tests/types-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import type {TypeWithDeepControls} from "storybook-addon-deep-controls"; 3 | import type {Meta, StoryObj} from "@storybook/react"; 4 | import type React from "react"; 5 | 6 | // NOTE: copy this file to other example packages to test types for different versions of Storybook 7 | 8 | // mocks just to structure tests 9 | function describe(name: string, fn: () => void): void {} 10 | function it(name: string, fn: () => void): void {} 11 | 12 | describe("Types", function () { 13 | describe("TypeWithDeepControls", function () { 14 | type Props = {bool: boolean; num: number}; 15 | type Cmp = React.ComponentType; 16 | type MetaType = TypeWithDeepControls>; 17 | type StoryType = TypeWithDeepControls>; 18 | 19 | it("story: works", function () { 20 | const _: StoryType = { 21 | args: {bool: true, num: 1}, 22 | argTypes: { 23 | bool: { 24 | type: "boolean", 25 | name: "name", 26 | description: "description", 27 | }, 28 | num: { 29 | type: {name: "number"}, 30 | name: "name", 31 | description: "description", 32 | }, 33 | "obj.bool": { 34 | type: "boolean", 35 | name: "name", 36 | description: "description", 37 | }, 38 | }, 39 | parameters: { 40 | deepControls: {enabled: true}, 41 | }, 42 | }; 43 | }); 44 | 45 | it("meta: works", function () { 46 | const _: MetaType = { 47 | args: {bool: true, num: 1}, 48 | argTypes: { 49 | bool: { 50 | type: "boolean", 51 | name: "name", 52 | description: "description", 53 | }, 54 | num: { 55 | type: {name: "number"}, 56 | name: "name", 57 | description: "description", 58 | }, 59 | "obj.bool": { 60 | type: "boolean", 61 | name: "name", 62 | description: "description", 63 | }, 64 | }, 65 | parameters: { 66 | deepControls: {enabled: true}, 67 | }, 68 | }; 69 | }); 70 | 71 | it("story: checks parameters types", function () { 72 | const _: StoryType = { 73 | parameters: { 74 | deepControls: { 75 | // @ts-expect-error - should be boolean 76 | enabled: "true", 77 | }, 78 | }, 79 | }; 80 | }); 81 | 82 | it("meta: checks parameters types", function () { 83 | const _: MetaType = { 84 | parameters: { 85 | deepControls: { 86 | // @ts-expect-error - should be boolean 87 | enabled: "true", 88 | }, 89 | }, 90 | }; 91 | }); 92 | 93 | it("story: checks args types", function () { 94 | const _: StoryType = { 95 | args: { 96 | // @ts-expect-error - should be boolean 97 | bool: "true", 98 | // @ts-expect-error - should be number 99 | num: true, 100 | // allows unknown args 101 | unknown: "foo", 102 | }, 103 | }; 104 | }); 105 | 106 | it("meta: checks args types", function () { 107 | const _: MetaType = { 108 | args: { 109 | // @ts-expect-error - should be boolean 110 | bool: "true", 111 | // @ts-expect-error - should be number 112 | num: true, 113 | // allows unknown args 114 | unknown: "foo", 115 | }, 116 | }; 117 | }); 118 | 119 | it("story: does not allow unknown argTypes without dot notation", function () { 120 | const _: StoryType = { 121 | argTypes: { 122 | // @ts-expect-error - unknown argTypes not allowed 123 | unknown: { 124 | control: "text", 125 | name: "name", 126 | description: "description", 127 | }, 128 | // arg types with dot notation allowed 129 | "obj.bool": { 130 | control: "boolean", 131 | name: "name", 132 | description: "description", 133 | }, 134 | }, 135 | }; 136 | }); 137 | 138 | it("meta: does not allow unknown argTypes without dot notation", function () { 139 | const _: MetaType = { 140 | argTypes: { 141 | // @ts-expect-error - unknown argTypes not allowed 142 | unknown: { 143 | control: "text", 144 | name: "name", 145 | description: "description", 146 | }, 147 | // arg types with dot notation allowed 148 | "obj.bool": { 149 | control: "boolean", 150 | name: "name", 151 | description: "description", 152 | }, 153 | }, 154 | }; 155 | }); 156 | 157 | it("story: checks argTypes types", function () { 158 | const _: StoryType = { 159 | argTypes: { 160 | bool: { 161 | // @ts-expect-error - unknown control type 162 | type: "unknown", 163 | // @ts-expect-error - should be string 164 | name: 1, 165 | // @ts-expect-error - should be string 166 | description: 1, 167 | }, 168 | num: { 169 | type: { 170 | // @ts-expect-error - unknown control type 171 | name: "unknown", 172 | }, 173 | // @ts-expect-error - should be string 174 | name: 1, 175 | // @ts-expect-error - should be string 176 | description: 1, 177 | }, 178 | "obj.bool": { 179 | // @ts-expect-error - unknown control type 180 | type: "unknown", 181 | // @ts-expect-error - should be string 182 | name: 1, 183 | // @ts-expect-error - should be string 184 | description: 1, 185 | }, 186 | "obj.num": { 187 | type: { 188 | // @ts-expect-error - unknown control type 189 | name: "unknown", 190 | }, 191 | // @ts-expect-error - should be string 192 | name: 1, 193 | // @ts-expect-error - should be string 194 | description: 1, 195 | }, 196 | "obj.nums": { 197 | // @ts-expect-error - missing value property 198 | type: { 199 | name: "array", 200 | }, 201 | // @ts-expect-error - should be string 202 | name: 1, 203 | // @ts-expect-error - should be string 204 | description: 1, 205 | }, 206 | }, 207 | }; 208 | }); 209 | 210 | it("meta: checks argTypes types", function () { 211 | const _: MetaType = { 212 | argTypes: { 213 | bool: { 214 | // @ts-expect-error - unknown control type 215 | type: "unknown", 216 | // @ts-expect-error - should be string 217 | name: 1, 218 | // @ts-expect-error - should be string 219 | description: 1, 220 | }, 221 | num: { 222 | type: { 223 | // @ts-expect-error - unknown control type 224 | name: "unknown", 225 | }, 226 | // @ts-expect-error - should be string 227 | name: 1, 228 | // @ts-expect-error - should be string 229 | description: 1, 230 | }, 231 | "obj.bool": { 232 | // @ts-expect-error - unknown control type 233 | type: "unknown", 234 | // @ts-expect-error - should be string 235 | name: 1, 236 | // @ts-expect-error - should be string 237 | description: 1, 238 | }, 239 | "obj.num": { 240 | type: { 241 | // @ts-expect-error - unknown control type 242 | name: "unknown", 243 | }, 244 | // @ts-expect-error - should be string 245 | name: 1, 246 | // @ts-expect-error - should be string 247 | description: 1, 248 | }, 249 | "obj.nums": { 250 | // @ts-expect-error - missing value property 251 | type: { 252 | name: "array", 253 | }, 254 | // @ts-expect-error - should be string 255 | name: 1, 256 | // @ts-expect-error - should be string 257 | description: 1, 258 | }, 259 | }, 260 | }; 261 | }); 262 | 263 | it("story: allows partial argTypes types", function () { 264 | const _: StoryType = { 265 | argTypes: { 266 | bool: {description: "description"}, 267 | num: {description: "description"}, 268 | "obj.bool": {description: "description"}, 269 | }, 270 | }; 271 | }); 272 | 273 | it("meta: allows partial argTypes types", function () { 274 | const _: MetaType = { 275 | argTypes: { 276 | bool: {description: "description"}, 277 | num: {description: "description"}, 278 | "obj.bool": {description: "description"}, 279 | }, 280 | }; 281 | }); 282 | }); 283 | }); 284 | --------------------------------------------------------------------------------