Skip to content

Commit

Permalink
use test-d format for type testing
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperpeulen committed Oct 4, 2022
1 parent 6f92476 commit 8f4dee7
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 165 deletions.
126 changes: 126 additions & 0 deletions code/renderers/react/src/__test-dts__/CSF3.test-d.tsx
Original file line number Diff line number Diff line change
@@ -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<Meta<typeof Button>>()({
component: Button,
args: { label: 'good', disabled: false },
});

type Story = StoryObj<typeof meta>;
const Basic: Story = {};
});

test('✅ Required args may be provided partial in meta and the story', () => {
const meta = satisfies<Meta<typeof Button>>()({
component: Button,
args: { label: 'good' },
});
const Basic: StoryObj<typeof meta> = {
args: { disabled: false },
};
});

test('❌ The combined shape of meta args and story args must match the required args.', () => {
{
const meta = satisfies<Meta<typeof Button>>()({ component: Button });
const Basic: StoryObj<typeof meta> = {
// @ts-expect-error disabled not provided ❌
args: { label: 'good' },
};
}
{
const meta = satisfies<Meta<typeof Button>>()({
component: Button,
args: { label: 'good' },
});
// @ts-expect-error disabled not provided ❌
const Basic: StoryObj<typeof meta> = {};
}
{
const meta = satisfies<Meta<ButtonProps>>()({ component: Button });
const Basic: StoryObj<typeof meta> = {
// @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<Meta<CmpProps>>()({
component: Cmp,
args: { label: 'good' },
});

const Basic: StoryObj<typeof meta> = {
args: { disabled: false, onLoading: () => <div>Loading...</div> },
};
});

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<Meta<Props>>()({
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 (
<Theme theme={args.theme}>
<Component {...args} />
</Theme>
);
},
});

const Basic: StoryObj<typeof meta> = { 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.
<Story args={{ decoratorArg: 0 }} />
</>
);

const meta = satisfies<Meta<Props>>()({
component: Button,
args: { label: 'good', disabled: false },
decorators: [withDecorator],
});

// Yes, decorator arg is required
const Basic: StoryObj<typeof meta> = { args: { decoratorArg: 0 } };
});
});
14 changes: 14 additions & 0 deletions code/renderers/react/src/__test-dts__/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T>(value: T): void {}
type IsAny<T> = T extends true ? (T extends false ? true : never) : never;

/**
* Mimicking the satisfies operator.
*/
export function satisfies<A>() {
return <T extends A>(x: T) => x;
}
109 changes: 0 additions & 109 deletions code/renderers/react/src/__test__/CSF3.typetest.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion code/renderers/react/src/public-api.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<typeof addDecorator>[0];
export type DecoratorFn<TArgs = Args> = DecoratorFunction<ReactFramework, TArgs>;
export const addParameters: ClientApi['addParameters'] = api.clientApi
.addParameters as ClientApi['addParameters'];
export const clearDecorators: ClientApi['clearDecorators'] = api.clientApi.clearDecorators;
Expand Down
67 changes: 12 additions & 55 deletions code/renderers/react/src/public-types.ts
Original file line number Diff line number Diff line change
@@ -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<any>;

Expand Down Expand Up @@ -28,64 +29,20 @@ export type StoryFn<TArgs = Args> = AnnotatedStoryFn<ReactFramework, TArgs>;
*
* @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
*/
export type StoryObj<TArgs = Args> = StoryAnnotations<ReactFramework, TArgs>;

/**
* For the common case where a component's stories are simple components that receives args as props:
*
* ```tsx
* export default { ... } as ComponentMeta<typeof Button>;
* ```
*/
export type ComponentMeta<T extends JSXElement> = Meta<ComponentProps<T>>;

/**
* For the common case where a (CSFv2) story is a simple component that receives args as props:
*
* ```tsx
* const Template: ComponentStoryFn<typeof Button> = (args) => <Button {...args} />
* ```
*/
export type ComponentStoryFn<T extends JSXElement> = StoryFn<ComponentProps<T>>;

/**
* For the common case where a (CSFv3) story is a simple component that receives args as props:
*
* ```tsx
* const MyStory: ComponentStoryObj<typeof Button> = {
* args: { buttonArg1: 'val' },
* }
* ```
*/
export type ComponentStoryObj<T extends JSXElement> = StoryObj<ComponentProps<T>>;

/**
* Story function that represents a CSFv3 component example.
*
* @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
*/
export type Story<
MetaOrArgs = Args,
StoryArgs = MetaOrArgs extends { component: ComponentType<infer CmpArgs> } ? CmpArgs : MetaOrArgs
> = MetaOrArgs extends {
component: ComponentType<infer CmpArgs>;
export type StoryObj<MetaOrArgs = Args> = MetaOrArgs extends {
render?: ArgsStoryFn<ReactFramework, infer StoryArgs>;
decorators?: DecoratorFunction<ReactFramework, infer StoryArgs>[];
component?: ComponentType<infer CmpArgs>;
args?: infer D;
}
? StoryAnnotations<ReactFramework<CmpArgs>, StoryArgs> &
({} extends MakeOptional<StoryArgs, D & ActionArgs<StoryArgs>>
? unknown
: { args: MakeOptional<StoryArgs, D & ActionArgs<StoryArgs>> })
? (unknown extends StoryArgs ? CmpArgs : StoryArgs) extends infer Args
? StoryAnnotations<ReactFramework<CmpArgs>, Args> & StrictStoryArgs<Args, D>
: never
: StoryAnnotations<ReactFramework, MetaOrArgs>;

/**
* For the common case where a (CSFv3) story is a simple component that receives args as props:
*
* ```tsx
* const MyStory: ComponentStory<typeof Button> = {
* args: { buttonArg1: 'val' },
* }
* ```
*/ export type ComponentStory<T extends JSXElement> = ComponentStoryObj<T>;
type StrictStoryArgs<Args, D> = {} extends MakeOptional<Args, D & ActionArgs<Args>>
? { args?: Partial<Args> }
: { args: MakeOptional<Args, D & ActionArgs<Args>> };

type ActionArgs<Args> = {
[P in keyof Args as ((...args: any[]) => void) extends Args[P] ? P : never]: Args[P];
Expand Down

0 comments on commit 8f4dee7

Please sign in to comment.