Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nova-react-test-utils): make types for reference entries stronger and error message more friendly #128

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "make types for reference entries stronger and error message more friendly",
"packageName": "@nova/react-test-utils",
"email": "[email protected]",
"dependentChangeType": "patch"
}
80 changes: 77 additions & 3 deletions packages/nova-react-test-utils/src/shared/types.d-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
: false;

import type { StoryObj, Meta } from "@storybook/react";
import type { StoryObjWithoutFragmentRefs } from "./types";
import type {
ComponentTypeError,
ParametersError,
ReferenceEntriesError,
StoryObjWithoutFragmentRefs,
} from "./types";
import type {
UnknownOperation,
WithNovaEnvironment,
Expand All @@ -30,6 +35,11 @@ type OptionalPropsOnly = {
optionalProp?: string;
};

type MultiplePropsFromDecorator = {
propPassedThroughDecorator: unknown;
anotherPropPassedThroughDecorator: unknown;
};

const Component: React.FC<Props> = (_: Props) => null;

const ComponentWithOptionalProps: React.FC<OptionalProps> = (
Expand All @@ -40,6 +50,10 @@ const ComponentWithOptionalPropsOnly: React.FC<OptionalPropsOnly> = (
_: OptionalPropsOnly,
) => null;

const ComponentWithMultiplePropsFromDecorator: React.FC<
MultiplePropsFromDecorator
> = (_: MultiplePropsFromDecorator) => null;

const parameters = {
novaEnvironment: {
query: {
Expand All @@ -51,6 +65,31 @@ const parameters = {
},
} satisfies WithNovaEnvironment<UnknownOperation>;

const parametersWithMultiplePropsCorrect = {
novaEnvironment: {
query: {
__brand: "GraphQLTaggedNode" as const,
},
referenceEntries: {
propPassedThroughDecorator: (data) => data,
anotherPropPassedThroughDecorator: (data) => data,
},
},
} satisfies WithNovaEnvironment<UnknownOperation>;

const parametersWithMultiplePropsWithRefEntryTypo = {
novaEnvironment: {
query: {
__brand: "GraphQLTaggedNode" as const,
},
referenceEntries: {
propPassedThroughDecorator: (data) => data,
anothPropPassedThroughDecorator: (data) => data,
someRandomProp: (data) => data,
},
},
} satisfies WithNovaEnvironment<UnknownOperation>;

const meta = {
component: Component,
parameters,
Expand Down Expand Up @@ -81,6 +120,20 @@ const metaForComponentWithOptionalPropsOnly = {
parameters,
} satisfies Meta<typeof ComponentWithOptionalPropsOnly>;

const metaWithoutComponent = {
parameters,
} satisfies Meta<typeof ComponentWithOptionalPropsOnly>;

const metaForMultiplePropsFromDecorator = {
component: ComponentWithMultiplePropsFromDecorator,
parameters: parametersWithMultiplePropsCorrect,
} satisfies Meta<typeof ComponentWithMultiplePropsFromDecorator>;

const metaWithIncorrectRefEntry = {
component: ComponentWithMultiplePropsFromDecorator,
parameters: parametersWithMultiplePropsWithRefEntryTypo,
} satisfies Meta<typeof ComponentWithMultiplePropsFromDecorator>;

type StoryObjForMeta = StoryObj<typeof meta>;
type StoryObjWithoutFragmentRefsForMeta = StoryObjWithoutFragmentRefs<
typeof meta
Expand Down Expand Up @@ -125,8 +178,29 @@ const StoryForComponentWithOptionalPropsOnly: StoryObjWithoutFragmentRefs<
},
};

type WithoutDecoratorParamsItShouldBeNever = Expect<
Equal<never, StoryObjWithoutFragmentRefs<typeof metaWithoutDecoratorParams>>
const StoryForComponentWithMultiplePropsFromDecorator: StoryObjWithoutFragmentRefs<
typeof metaForMultiplePropsFromDecorator
> = {};

type WithoutDecoratorParamsItShouldBeParametersError = Expect<
Equal<
ParametersError,
StoryObjWithoutFragmentRefs<typeof metaWithoutDecoratorParams>
>
>;

type WithoutComponentItShouldBeComponentError = Expect<
Equal<
ComponentTypeError,
StoryObjWithoutFragmentRefs<typeof metaWithoutComponent>
>
>;

type WithTypoInRefEntryItShouldBeReferenceEntriesError = Expect<
Equal<
ReferenceEntriesError<"anothPropPassedThroughDecorator" | "someRandomProp">,
StoryObjWithoutFragmentRefs<typeof metaWithIncorrectRefEntry>
>
>;

type VerifyTypeMatchesWithNoArgsOnMeta = Expect<
Expand Down
33 changes: 27 additions & 6 deletions packages/nova-react-test-utils/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type OptionalArgs<T> = T extends { args?: infer A }
: never
: never;

// Omits Z from the Storybook "args" field of T
type OmitFromArgs<T, Z> = Omit<T, "args"> &
Simplify<
(Record<string, never> extends Omit<RequiredArgs<T>, keyof Z>
Expand All @@ -22,11 +23,31 @@ type OmitFromArgs<T, Z> = Omit<T, "args"> &
{ deep: true }
>;

// Check if keys of T are a subset of keys of P. Returns true if they are or union of keys that are not.
type AreKeysSubset<T extends object, P extends object> = {
[K in keyof T]: K extends keyof P ? never : K;
}[keyof T] extends never
? true
: {
[K in keyof T]: K extends keyof P ? never : K;
}[keyof T];

export type ParametersError =
"❌ Error: The type passed to StoryObjWithoutFragmentRefs needs to have a parameters field with novaEnvironment with referenceEntries field";

export type ComponentTypeError =
"❌ Error: The type passed to StoryObjWithoutFragmentRefs needs to have a component field that is a React component";

export type ReferenceEntriesError<K> = `❌ Error: The reference entry '${K &
string}' is not a property on the component's props`;

export type StoryObjWithoutFragmentRefs<T> = T extends {
component: infer C;
parameters: { novaEnvironment: { referenceEntries: infer D } };
component?: infer C;
parameters: { novaEnvironment: { referenceEntries: infer D extends object } };
}
? C extends React.ComponentType<any>
? OmitFromArgs<StoryObj<T>, D>
: never
: never;
? C extends React.ComponentType<infer P extends object>
? AreKeysSubset<D, P> extends true
? OmitFromArgs<StoryObj<T>, D>
: ReferenceEntriesError<AreKeysSubset<D, P>>
: ComponentTypeError
: ParametersError;
Loading