Skip to content

Commit

Permalink
Merge pull request #49 from ComponentDriven/kasper/sb-738/csf-3
Browse files Browse the repository at this point in the history
Sound arg types for CSF3
  • Loading branch information
shilman authored Oct 22, 2022
2 parents 3bd2bd7 + 084d9eb commit 65292c2
Show file tree
Hide file tree
Showing 11 changed files with 3,303 additions and 5,010 deletions.
6 changes: 0 additions & 6 deletions .babelrc.js

This file was deleted.

1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test

on: [push]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x

- name: Install dependencies
uses: bahmutov/npm-install@v1

- name: Check typescript
run: |
yarn run check
- name: Check linter
run: |
yarn lint
- name: Run tests
run: |
yarn test
42 changes: 28 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,59 @@
"*.d.ts"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts\" && tsc --emitDeclarationOnly",
"lint": "eslint src --ext .js,.ts",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"check": "tsc",
"lint": "eslint src --ext .ts",
"test": "jest",
"release": "yarn build && auto shipit"
},
"eslintConfig": {
"extends": [
"@storybook/eslint-config-storybook"
]
],
"rules": {
"jest/expect-expect": [
"warn",
{
"assertFunctionNames": [
"expect",
"expectTypeOf"
]
}
]
}
},
"prettier": "@storybook/linter-config/prettier.config",
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"roots": [
"<rootDir>/src"
]
},
"dependencies": {
"lodash": "^4.17.15"
"expect-type": "^0.14.2",
"lodash": "^4.17.15",
"type-fest": "^2.19.0"
},
"devDependencies": {
"@auto-it/released": "^10.37.1",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/preset-typescript": "^7.9.0",
"@storybook/eslint-config-storybook": "^2.1.0",
"@types/jest": "^24.0.23",
"@types/jest": "^29.2.0",
"@types/lodash": "^4.14.149",
"@types/node": "^18.11.0",
"@typescript-eslint/parser": "^4.33.0",
"auto": "^10.31.0",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^24.9.0",
"common-tags": "^1.8.0",
"eslint": "^6.7.1",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"typescript": "^3.7.2"
"jest": "^29.2.0",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"tsup": "^6.3.0",
"typescript": "^4.8.4"
},
"auto": {
"plugins": [
Expand Down
21 changes: 8 additions & 13 deletions src/includeConditionalArg.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-ignore */
import { includeConditionalArg, testValue } from './includeConditionalArg';
import type { Conditional } from './story';

describe('testValue', () => {
describe('truthy', () => {
Expand Down Expand Up @@ -60,33 +61,27 @@ describe('testValue', () => {
describe('includeConditionalArg', () => {
describe('errors', () => {
it('should throw if neither arg nor global is specified', () => {
expect(() => includeConditionalArg({ if: {} }, {}, {})).toThrowErrorMatchingInlineSnapshot(
`"Invalid conditional value {}"`
);
expect(() =>
includeConditionalArg({ if: {} as Conditional }, {}, {})
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional value {}"`);
});
it('should throw if arg and global are both specified', () => {
expect(() =>
includeConditionalArg({ if: { arg: 'a', global: 'b' } }, {}, {})
).toThrowErrorMatchingInlineSnapshot(
`"Invalid conditional value {\\"arg\\":\\"a\\",\\"global\\":\\"b\\"}"`
);
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional value {"arg":"a","global":"b"}"`);
});
it('should throw if mulitiple exists / eq / neq are specified', () => {
expect(() =>
includeConditionalArg({ if: { arg: 'a', exists: true, eq: 1 } }, {}, {})
).toThrowErrorMatchingInlineSnapshot(
`"Invalid conditional test {\\"exists\\":true,\\"eq\\":1}"`
);
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional test {"exists":true,"eq":1}"`);

expect(() =>
includeConditionalArg({ if: { arg: 'a', exists: false, neq: 0 } }, {}, {})
).toThrowErrorMatchingInlineSnapshot(
`"Invalid conditional test {\\"exists\\":false,\\"neq\\":0}"`
);
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional test {"exists":false,"neq":0}"`);

expect(() =>
includeConditionalArg({ if: { arg: 'a', eq: 1, neq: 0 } }, {}, {})
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional test {\\"eq\\":1,\\"neq\\":0}"`);
).toThrowErrorMatchingInlineSnapshot(`"Invalid conditional test {"eq":1,"neq":0}"`);
});
});

Expand Down
3 changes: 2 additions & 1 deletion src/includeConditionalArg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import isEqual from 'lodash/isEqual';
import { Args, Globals, InputType, Conditional } from './story';

const count = (vals: any[]) => vals.map(v => typeof v !== 'undefined').filter(Boolean).length;
const count = (vals: any[]) => vals.map((v) => typeof v !== 'undefined').filter(Boolean).length;

export const testValue = (cond: Omit<Conditional, 'arg' | 'global'>, value: any) => {
const { exists, eq, neq, truthy } = cond as any;
Expand Down Expand Up @@ -35,5 +35,6 @@ export const includeConditionalArg = (argType: InputType, args: Args, globals: G
}

const value = arg ? args[arg] : globals[global];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return testValue(argType.if!, value);
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface SeparatorOptions {
*/
export const parseKind = (kind: string, { rootSeparator, groupSeparator }: SeparatorOptions) => {
const [root, remainder] = kind.split(rootSeparator, 2);
const groups = (remainder || kind).split(groupSeparator).filter(i => !!i);
const groups = (remainder || kind).split(groupSeparator).filter((i) => !!i);

// when there's no remainder, it means the root wasn't found/split
return {
Expand Down
72 changes: 56 additions & 16 deletions src/story.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
import { Args, ComponentAnnotations, StoryAnnotationsOrFn, ProjectAnnotations } from './story';
import { expectTypeOf } from 'expect-type';
import {
AnyFramework,
Args,
ArgsFromMeta,
ArgsStoryFn,
ComponentAnnotations,
DecoratorFunction,
LoaderFunction,
ProjectAnnotations,
StoryAnnotationsOrFn,
} from './story';

// NOTE Example of internal type definition for @storybook/<X> (where X is a framework)
type XFramework = {
component: () => string;
interface XFramework extends AnyFramework {
component: (args: this['T']) => string;
storyResult: string;
};
}

type XMeta<TArgs = Args> = ComponentAnnotations<XFramework, TArgs>;
type XStory<TArgs = Args> = StoryAnnotationsOrFn<XFramework, TArgs>;

// NOTE Examples of using types from @storybook/<X> in real project

const Button: XFramework['component'] = () => 'Button';
type ButtonArgs = {
x: string;
y: string;
};

const Button = (props: ButtonArgs) => 'Button';

// NOTE Various kind usages
const simple: XMeta = {
title: 'simple',
component: Button,
decorators: [(storyFn, context) => `withDecorator(${storyFn(context)})`],
parameters: { a: () => null, b: NaN, c: Symbol('symbol') },
loaders: [() => Promise.resolve({ d: '3' })],
args: { a: 1 },
argTypes: { a: { type: { name: 'string' } } },
args: { x: '1' },
argTypes: { x: { type: { name: 'string' } } },
};

const strict: XMeta<ButtonArgs> = {
Expand All @@ -44,32 +56,32 @@ const Simple: XStory = () => 'Simple';
const CSF1Story: XStory = () => 'Named Story';
CSF1Story.story = {
name: 'Another name for story',
decorators: [storyFn => `Wrapped(${storyFn()}`],
decorators: [(storyFn) => `Wrapped(${storyFn()}`],
parameters: { a: [1, '2', {}], b: undefined, c: Button },
loaders: [() => Promise.resolve({ d: '3' })],
args: { a: 1 },
};

const CSF2Story: XStory = () => 'Named Story';
CSF2Story.storyName = 'Another name for story';
CSF2Story.decorators = [storyFn => `Wrapped(${storyFn()}`];
CSF2Story.decorators = [(storyFn) => `Wrapped(${storyFn()}`];
CSF2Story.parameters = { a: [1, '2', {}], b: undefined, c: Button };
CSF2Story.loaders = [() => Promise.resolve({ d: '3' })];
CSF2Story.args = { a: 1 };

const CSF3Story: XStory = {
render: args => 'Named Story',
render: (args) => 'Named Story',
name: 'Another name for story',
decorators: [storyFn => `Wrapped(${storyFn()}`],
decorators: [(storyFn) => `Wrapped(${storyFn()}`],
parameters: { a: [1, '2', {}], b: undefined, c: Button },
loaders: [() => Promise.resolve({ d: '3' })],
args: { a: 1 },
};

const CSF3StoryStrict: XStory<ButtonArgs> = {
render: args => 'Named Story',
render: (args) => 'Named Story',
name: 'Another name for story',
decorators: [storyFn => `Wrapped(${storyFn()}`],
decorators: [(storyFn) => `Wrapped(${storyFn()}`],
parameters: { a: [1, '2', {}], b: undefined, c: Button },
loaders: [() => Promise.resolve({ d: '3' })],
args: { x: '1' },
Expand All @@ -86,7 +98,35 @@ const project: ProjectAnnotations<XFramework> = {
},
};

// NOTE Jest forced to define at least one test in file
describe('story', () => {
test('true', () => expect(true).toBe(true));
test('ArgsFromMeta will infer correct args from render/loader/decorators', () => {
const decorator1: DecoratorFunction<XFramework, { decoratorArg: string }> = (Story, { args }) =>
`${args.decoratorArg}`;

const decorator2: DecoratorFunction<XFramework, { decoratorArg2: string }> = (Story, { args }) =>
`${args.decoratorArg2}`;

const loader: LoaderFunction<XFramework, { loaderArg: number }> = async ({ args }) => ({
loader: `${args.loaderArg}`,
});

const loader2: LoaderFunction<XFramework, { loaderArg2: number }> = async ({ args }) => ({
loader2: `${args.loaderArg2}`,
});

const renderer: ArgsStoryFn<XFramework, { theme: string }> = (args) => `${args.theme}`;

const meta = {
component: Button,
args: { disabled: false },
render: renderer,
decorators: [decorator1, decorator2],
loaders: [loader, loader2],
};
expectTypeOf<ArgsFromMeta<XFramework, typeof meta>>().toEqualTypeOf<{
theme: string;
decoratorArg: string;
decoratorArg2: string;
loaderArg: number;
loaderArg2: number;
}>();
});
Loading

0 comments on commit 65292c2

Please sign in to comment.