From 8f4dee7b49779fc8445bcce6411d4504b34d6340 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Tue, 4 Oct 2022 17:26:34 +0200 Subject: [PATCH] use test-d format for type testing --- .../react/src/__test-dts__/CSF3.test-d.tsx | 126 ++++++++++++++++++ .../renderers/react/src/__test-dts__/utils.ts | 14 ++ .../react/src/__test__/CSF3.typetest.tsx | 109 --------------- code/renderers/react/src/public-api.tsx | 3 +- code/renderers/react/src/public-types.ts | 67 ++-------- 5 files changed, 154 insertions(+), 165 deletions(-) create mode 100644 code/renderers/react/src/__test-dts__/CSF3.test-d.tsx create mode 100644 code/renderers/react/src/__test-dts__/utils.ts delete mode 100644 code/renderers/react/src/__test__/CSF3.typetest.tsx diff --git a/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx b/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx new file mode 100644 index 000000000000..7f1d97c40bcf --- /dev/null +++ b/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx @@ -0,0 +1,126 @@ +/* eslint-disable jest/expect-expect */ +import React, { KeyboardEventHandler, ReactNode } from 'react'; +import { Meta, StoryObj } from '../public-types'; +import { DecoratorFn } from '../public-api'; +import { describe, satisfies, test } from './utils'; + +type ButtonProps = { label: string; disabled: boolean }; +const Button: (props: ButtonProps) => JSX.Element = () => <>; + +describe('Args can be provided in multiple ways', () => { + test('✅All required args may be provided in meta', () => { + const meta = satisfies>()({ + component: Button, + args: { label: 'good', disabled: false }, + }); + + type Story = StoryObj; + const Basic: Story = {}; + }); + + test('✅ Required args may be provided partial in meta and the story', () => { + const meta = satisfies>()({ + component: Button, + args: { label: 'good' }, + }); + const Basic: StoryObj = { + args: { disabled: false }, + }; + }); + + test('❌ The combined shape of meta args and story args must match the required args.', () => { + { + const meta = satisfies>()({ component: Button }); + const Basic: StoryObj = { + // @ts-expect-error disabled not provided ❌ + args: { label: 'good' }, + }; + } + { + const meta = satisfies>()({ + component: Button, + args: { label: 'good' }, + }); + // @ts-expect-error disabled not provided ❌ + const Basic: StoryObj = {}; + } + { + const meta = satisfies>()({ component: Button }); + const Basic: StoryObj = { + // @ts-expect-error disabled not provided ❌ + args: { label: 'good' }, + }; + } + }); +}); + +test('✅ All void functions are optional', () => { + interface CmpProps { + label: string; + disabled: boolean; + onClick(): void; + onKeyDown: KeyboardEventHandler; + onLoading: (s: string) => JSX.Element; + submitAction(): void; + } + + const Cmp: (props: CmpProps) => JSX.Element = () => <>; + + const meta = satisfies>()({ + component: Cmp, + args: { label: 'good' }, + }); + + const Basic: StoryObj = { + args: { disabled: false, onLoading: () =>
Loading...
}, + }; +}); + +type ThemeData = 'light' | 'dark'; +declare const Theme: (props: { theme: ThemeData; children?: ReactNode }) => JSX.Element; + +describe('Story args can be inferred', () => { + test('Correct args are inferred when type is widened for render function', () => { + type Props = ButtonProps & { theme: ThemeData }; + + const meta = satisfies>()({ + component: Button, + args: { label: 'good', disabled: false }, + render: (args, { component }) => { + // TODO: Might be nice if we can infer that. + // component is not null as it is provided in meta + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const Component = component!; + return ( + + + + ); + }, + }); + + const Basic: StoryObj = { args: { theme: 'light' } }; + }); + + test('Correct args are inferred when type is widened for decorators', () => { + type Props = ButtonProps & { decoratorArg: number }; + + const withDecorator: DecoratorFn<{ decoratorArg: number }> = (Story, { args }) => ( + <> + Decorator: {args.decoratorArg} + This Story allows optional TArgs, but the decorator only knows about the decoratorArg. It + should really allow optionally a Partial of TArgs. + + + ); + + const meta = satisfies>()({ + component: Button, + args: { label: 'good', disabled: false }, + decorators: [withDecorator], + }); + + // Yes, decorator arg is required + const Basic: StoryObj = { args: { decoratorArg: 0 } }; + }); +}); diff --git a/code/renderers/react/src/__test-dts__/utils.ts b/code/renderers/react/src/__test-dts__/utils.ts new file mode 100644 index 000000000000..adeeadb12242 --- /dev/null +++ b/code/renderers/react/src/__test-dts__/utils.ts @@ -0,0 +1,14 @@ +// Inspired by: https://github.com/vuejs/core/blob/main/test-dts/index.d.ts + +export function describe(name: string, fn: () => void): void {} +export function test(name: string, fn: () => void): void {} + +export function expectAssignable(value: T): void {} +type IsAny = T extends true ? (T extends false ? true : never) : never; + +/** + * Mimicking the satisfies operator. + */ +export function satisfies() { + return (x: T) => x; +} diff --git a/code/renderers/react/src/__test__/CSF3.typetest.tsx b/code/renderers/react/src/__test__/CSF3.typetest.tsx deleted file mode 100644 index ee1a7aab8f9b..000000000000 --- a/code/renderers/react/src/__test__/CSF3.typetest.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { ComponentType, KeyboardEventHandler, ReactElement, ReactNode } from 'react'; -import { Meta, Story } from '../public-types'; - -interface ButtonProps { - label: string; - disabled: boolean; -} - -declare const Button: (props: ButtonProps) => JSX.Element; - -/** - * Mimicking the satisfies operator. - */ -function satisfies() { - return (x: T) => x; -} - -// ✅ valid -export const meta1 = satisfies>()({ - component: Button, - args: { label: 'good', disabled: false }, -}); - -export const Basic1: Story = {}; - -// // ✅ valid -export const meta2 = satisfies>()({ - component: Button, - args: { label: 'good' }, -}); - -export const Basic2: Story = { - args: { disabled: false }, -}; - -// ❌ invalid -const meta3 = satisfies>()({ - component: Button, -}); - -export const Basic3: Story = { - // @ts-expect-error disabled not provided - args: { label: 'good' }, -}; - -// ❌ invalid -const meta4 = satisfies>()({ - component: Button, - args: { label: 'good' }, -}); - -// @ts-expect-error disabled not provided -export const Basic4: Story = {}; - -// ❌ invalid -const meta5 = satisfies>()({ - component: Button, -}); - -export const Basic5: Story = { - // @ts-expect-error disabled not provided - args: { label: 'good' }, -}; - -interface ButtonProps2 { - label: string; - disabled: boolean; - onClick(): void; - onKeyDown: KeyboardEventHandler; - onLoading: (s: string) => JSX.Element; - submitAction(): void; -} - -declare const Button2: (props: ButtonProps2) => JSX.Element; - -// ✅ valid -const meta6 = satisfies>()({ - component: Button2, - args: { label: 'good' }, -}); - -export const Basic6: Story = { - // all functions are optional, except onLoading - args: { disabled: false, onLoading: () =>
Loading...
}, -}; - -type ThemeData = 'light' | 'dark'; -declare const Theme: (props: { theme: ThemeData; children?: ReactNode }) => JSX.Element; -type Props = ButtonProps & { theme: ThemeData }; - -export const meta7 = satisfies>()({ - component: Button, - args: { label: 'good', disabled: false }, - render: (args, { component }) => { - // component is not null as it is provided in meta - // TODO: Might be nice if we can infer that. - // eslint-disable-next-line - const Component = component!; - return ( - - - - ); - }, -}); - -export const Basic: Story = { - args: { theme: 'light' }, -}; diff --git a/code/renderers/react/src/public-api.tsx b/code/renderers/react/src/public-api.tsx index 651221a468b9..f53845a4c419 100644 --- a/code/renderers/react/src/public-api.tsx +++ b/code/renderers/react/src/public-api.tsx @@ -1,6 +1,7 @@ /* eslint-disable prefer-destructuring */ import { start } from '@storybook/core-client'; import type { ClientStoryApi, Loadable } from '@storybook/addons'; +import type { Args, DecoratorFunction } from '@storybook/csf'; import { renderToDOM, render } from './render'; import type { IStorybookSection, ReactFramework } from './types'; @@ -26,7 +27,7 @@ export const storiesOf: ClientApi['storiesOf'] = (kind, m) => { export const configure: ClientApi['configure'] = (...args) => api.configure(FRAMEWORK, ...args); export const addDecorator: ClientApi['addDecorator'] = api.clientApi .addDecorator as ClientApi['addDecorator']; -export type DecoratorFn = Parameters[0]; +export type DecoratorFn = DecoratorFunction; export const addParameters: ClientApi['addParameters'] = api.clientApi .addParameters as ClientApi['addParameters']; export const clearDecorators: ClientApi['clearDecorators'] = api.clientApi.clearDecorators; diff --git a/code/renderers/react/src/public-types.ts b/code/renderers/react/src/public-types.ts index dd7abbc53819..f8406ed20223 100644 --- a/code/renderers/react/src/public-types.ts +++ b/code/renderers/react/src/public-types.ts @@ -1,6 +1,7 @@ import { AnnotatedStoryFn, Args, ComponentAnnotations, StoryAnnotations } from '@storybook/csf'; -import { ComponentProps, ComponentType, JSXElementConstructor } from 'react'; +import { ComponentType, JSXElementConstructor } from 'react'; import { ReactFramework } from './types'; +import { ArgsStoryFn, DecoratorFunction } from '../../../../../csf/src'; type JSXElement = keyof JSX.IntrinsicElements | JSXElementConstructor; @@ -28,64 +29,20 @@ export type StoryFn = AnnotatedStoryFn; * * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) */ -export type StoryObj = StoryAnnotations; - -/** - * For the common case where a component's stories are simple components that receives args as props: - * - * ```tsx - * export default { ... } as ComponentMeta; - * ``` - */ -export type ComponentMeta = Meta>; - -/** - * For the common case where a (CSFv2) story is a simple component that receives args as props: - * - * ```tsx - * const Template: ComponentStoryFn = (args) =>