= {
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 |
--------------------------------------------------------------------------------