Skip to content

Commit

Permalink
fix(nova-react-test-utils): improve types for WithoutFragmentRefs (#114)
Browse files Browse the repository at this point in the history
* improve WithoutRefs type

* improve the type

* Change files

* add tests for types

* relax the check

* more utils and tests

---------

Co-authored-by: Stanislaw Wilczynski <[email protected]>
  • Loading branch information
sjwilczynski and Stanislaw Wilczynski authored Sep 20, 2024
1 parent 0f28fef commit f586941
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "use imporved type",
"packageName": "@nova/examples",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "improve type for StoryObj",
"packageName": "@nova/react-test-utils",
"email": "[email protected]",
"dependentChangeType": "patch"
}
10 changes: 1 addition & 9 deletions packages/examples/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@ export default {
// Needed to load relay compiler generated artifacts.
".+[\\\\/]relay[\\\\/].+\\.tsx?$":
"@graphitation/embedded-document-artefact-loader/ts-jest",
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: {
// prevents error about graphql import not being used when artifacts are loaded
noUnusedLocals: false,
},
},
],
"^.+\\.tsx?$": ["ts-jest"],
},
};
12 changes: 6 additions & 6 deletions packages/examples/src/apollo/Feedback/Feedback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
getNovaDecorator,
getNovaEnvironmentForStory,
type WithNovaEnvironment,
type WithoutFragmentRefs,
type StoryObjWithoutFragmentRefs,
} from "@nova/react-test-utils/apollo";
import type { Meta, StoryObj } from "@storybook/react";
import type { Meta } from "@storybook/react";
import { expect, userEvent, waitFor, within } from "@storybook/test";
import { getSchema } from "../../testing-utils/getSchema";
import type { TypeMap } from "../../__generated__/schema.all.interface";
Expand Down Expand Up @@ -37,7 +37,7 @@ const meta = {
} satisfies Meta<typeof FeedbackComponent>;

export default meta;
type Story = StoryObj<WithoutFragmentRefs<typeof meta>>;
type Story = StoryObjWithoutFragmentRefs<typeof meta>;

export const AutoGeneratedDataOnly: Story = {};

Expand Down Expand Up @@ -104,7 +104,9 @@ export const ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError
},
};

const FeedbackWithDeleteDialog = (props: Story["args"]) => {
const FeedbackWithDeleteDialog = (
props: React.ComponentProps<typeof FeedbackComponent>,
) => {
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
Expand All @@ -117,8 +119,6 @@ const FeedbackWithDeleteDialog = (props: Story["args"]) => {
},
}}
>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore we know feedback is passed through decorator */}
<FeedbackComponent {...props} />

<dialog open={open}>
Expand Down
12 changes: 6 additions & 6 deletions packages/examples/src/relay/Feedback/Feedback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {
getNovaDecorator,
getNovaEnvironmentForStory,
MockPayloadGenerator as PayloadGenerator,
type WithoutFragmentRefs,
type StoryObjWithoutFragmentRefs,
type WithNovaEnvironment,
EventingProvider,
} from "@nova/react-test-utils/relay";
import type { Meta, StoryObj } from "@storybook/react";
import type { Meta } from "@storybook/react";
import { userEvent, waitFor, within, expect } from "@storybook/test";
import type { TypeMap } from "../../__generated__/schema.all.interface";
import { FeedbackComponent } from "./Feedback";
Expand Down Expand Up @@ -41,7 +41,7 @@ const meta = {
} satisfies Meta<typeof FeedbackComponent>;

export default meta;
type Story = StoryObj<WithoutFragmentRefs<typeof meta>>;
type Story = StoryObjWithoutFragmentRefs<typeof meta>;

export const AutoGeneratedDataOnly: Story = {};

Expand Down Expand Up @@ -122,7 +122,9 @@ export const ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError
},
};

const FeedbackWithDeleteDialog = (props: Story["args"]) => {
const FeedbackWithDeleteDialog = (
props: React.ComponentProps<typeof FeedbackComponent>,
) => {
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
Expand All @@ -135,8 +137,6 @@ const FeedbackWithDeleteDialog = (props: Story["args"]) => {
},
}}
>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore we know feedback is passed through decorator */}
<FeedbackComponent {...props} />
<dialog open={open}>
<button onClick={() => setOpen(false)}>Cancel</button>
Expand Down
12 changes: 6 additions & 6 deletions packages/examples/src/relay/pure-relay/Feedback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import {
getNovaDecorator,
getNovaEnvironmentForStory,
MockPayloadGenerator as PayloadGenerator,
type WithoutFragmentRefs,
type WithNovaEnvironment,
EventingProvider,
getOperationName,
getOperationType,
type StoryObjWithoutFragmentRefs,
} from "@nova/react-test-utils/relay";
import type { Meta, StoryObj } from "@storybook/react";
import type { Meta } from "@storybook/react";
import { userEvent, waitFor, within, expect } from "@storybook/test";
import type { TypeMap } from "../../__generated__/schema.all.interface";
import { FeedbackComponent } from "./Feedback";
Expand Down Expand Up @@ -43,7 +43,7 @@ const meta = {
} satisfies Meta<typeof FeedbackComponent>;

export default meta;
type Story = StoryObj<WithoutFragmentRefs<typeof meta>>;
type Story = StoryObjWithoutFragmentRefs<typeof meta>;

export const AutoGeneratedDataOnly: Story = {};

Expand Down Expand Up @@ -166,7 +166,9 @@ export const LikeFailure: Story = {
},
};

const FeedbackWithDeleteDialog = (props: Story["args"]) => {
const FeedbackWithDeleteDialog = (
props: React.ComponentProps<typeof FeedbackComponent>,
) => {
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
Expand All @@ -179,8 +181,6 @@ const FeedbackWithDeleteDialog = (props: Story["args"]) => {
},
}}
>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore we know feedback is passed through decorator */}
<FeedbackComponent {...props} />
<dialog open={open}>
<button onClick={() => setOpen(false)}>Cancel</button>
Expand Down
33 changes: 22 additions & 11 deletions packages/nova-react-test-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ import type {

const schema = getSchema();

const meta: Meta<typeof FeedbackContainer> = {
const meta = {
component: FeedbackContainer,
decorators: [getNovaDecorator(schema)],
};
} satisfies Meta<typeof FeedbackContainer>;

export default meta;
type Story = StoryObj<typeof meta>;
Expand Down Expand Up @@ -107,7 +107,7 @@ export const LikeFailure: Story = {
novaEnvironment: {
enableQueuedMockResolvers: false,
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
} satisfies WithNovaEnvironment<UnknownOperation, TypeMap>,
play: async (context) => {
const {
graphql: { mock },
Expand Down Expand Up @@ -141,7 +141,7 @@ In addition, if your component only defines a GraphQL fragment and does not perf
Here's an example of a story for a component which only contains a fragment:

```tsx
const meta: Meta<typeof FeedbackComponent> = {
const meta = {
component: FeedbackComponent,
decorators: [getNovaDecorator(getSchema())],
parameters: {
Expand All @@ -157,23 +157,34 @@ const meta: Meta<typeof FeedbackComponent> = {
referenceEntries: {
feedback: (data) => data?.feedback,
},
resolvers: {
Feedback: () => sampleFeedback,
},
},
} satisfies WithNovaEnvironment<FeedbackStoryQuery, TypeMap>,
};
} satisfies Meta<typeof FeedbackComponent>;

type Story = StoryObjWithoutFragmentRefs<typeof meta>;

export const Primary: Story = {};
```

In this example, a wrapping component will execute the `query` with `useLazyLoadQuery` and passes the result to the `FeedbackComponent` via the `feedback` prop.

For Relay examples and more real life examples please check the [examples package](../examples/src/).

You can also see that `satisfies NovaEnvironmentDecoratorParameters<Operation, TypeMap>` is used to strongly type parameters. The `Operation` is used to make the `referenceEntries` prop strongly typed. If no `query` is defined it can be set to the `UnknownOperation` type exported from the package. The operation types are generated by the GraphQL compiler (either [apollo-react-relay-duct-tape-compiler](https://github.com/microsoft/graphitation/tree/main/packages/apollo-react-relay-duct-tape-compiler) or [relay-compiler](https://github.com/facebook/relay/tree/main/packages/relay-compiler)). The `TypeMap` type gives you strongly typed mock resolvers and can be generated using [typemap-plugin](https://github.com/microsoft/graphitation/tree/main/packages/graphql-codegen-typescript-typemap-plugin) that can be added to graphql codegen config file.
You can also see that `satisfies WithNovaEnvironment<Operation, TypeMap>` is used to strongly type parameters. The `Operation` is used to make the `referenceEntries` prop strongly typed. If no `query` is defined it can be set to the `UnknownOperation` type exported from the package. The operation types are generated by the GraphQL compiler (either [apollo-react-relay-duct-tape-compiler](https://github.com/microsoft/graphitation/tree/main/packages/apollo-react-relay-duct-tape-compiler) or [relay-compiler](https://github.com/facebook/relay/tree/main/packages/relay-compiler)). The `TypeMap` type gives you strongly typed mock resolvers and can be generated using [typemap-plugin](https://github.com/microsoft/graphitation/tree/main/packages/graphql-codegen-typescript-typemap-plugin) that can be added to graphql codegen config file.

Additionally, `StoryObjWithoutFragmentRefs` utility type is provided which is just a small wrapper over `StoryObj` type from Storybook that makes sure that `args` don't require you to pass props that are supplied using `referenceEntries` parameter.

### EventingProvider

This utility is meant to override default behavior for `bubble` which logs all nova events to Storybook actions tab. Instead you can granularly override the handler per event to customize your story. Check example below:

```tsx
const FeedbackWithDeleteDialog = (props: Story["args"]) => {
const FeedbackWithDeleteDialog = (
props: React.ComponentProps<typeof FeedbackComponent>,
) => {
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
Expand Down Expand Up @@ -231,14 +242,14 @@ const environment = createMockEnvironment(schema, {
and if you are using through storybook decorator you can pass options to `getNovaDecorator`:

```tsx
const meta: Meta<typeof FeedbackContainer> = {
const meta = {
component: FeedbackContainer,
decorators: [
getNovaDecorator(schema, {
cache: myCustomCacheConfig,
}),
],
};
} satisfies Meta<typeof FeedbackContainer>;
```

#### I need to configure Relay store to support some custom setup I have in my repository. Is it possible?
Expand All @@ -254,14 +265,14 @@ const environment = createMockEnvironment(schema, {
and if you are using through storybook decorator you can pass options to `getNovaDecorator`:

```tsx
const meta: Meta<typeof FeedbackContainer> = {
const meta = {
component: FeedbackContainer,
decorators: [
getNovaDecorator(schema, {
store: myCustomStoreConfig,
}),
],
};
} satisfies Meta<typeof FeedbackContainer>;
```

The second parameter of `getNovaDecorator` is an `options` object of type `Partial<EnvironmentConfig>` from `relay-test-utils`.
Expand Down
3 changes: 2 additions & 1 deletion packages/nova-react-test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"@types/relay-test-utils": ">16",
"graphql": "^15.5.0",
"invariant": "^2.2.4",
"tslib": "^2.2.0"
"tslib": "^2.2.0",
"type-fest": "~2.19"
},
"devDependencies": {
"@apollo/client": "^3.4.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/nova-react-test-utils/src/apollo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export { prepareStoryContextForTest } from "../shared/storybook-nova-decorator-s
export type {
WithNovaEnvironment,
UnknownOperation,
WithoutFragmentRefs,
MockResolvers,
DefaultMockResolvers,
} from "../shared/storybook-nova-decorator-shared";
export { EventingProvider } from "../shared/eventing-provider";
export type { StoryObjWithoutFragmentRefs } from "../shared/types";

export {
ApolloMockPayloadGenerator as MockPayloadGenerator,
Expand Down
2 changes: 1 addition & 1 deletion packages/nova-react-test-utils/src/relay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export { prepareStoryContextForTest } from "../shared/storybook-nova-decorator-s
export type {
WithNovaEnvironment,
UnknownOperation,
WithoutFragmentRefs,
MockResolvers,
DefaultMockResolvers,
} from "../shared/storybook-nova-decorator-shared";
export { EventingProvider } from "../shared/eventing-provider";
export type { StoryObjWithoutFragmentRefs } from "../shared/types";

export {
RelayMockPayloadGenerator as MockPayloadGenerator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@ export type WithNovaEnvironment<
);
};

export type WithoutFragmentRefs<T> = T extends {
component: infer C;
parameters: { novaEnvironment: { referenceEntries: infer D } };
}
? C extends React.ComponentType<infer P>
? Omit<P, keyof D>
: never
: never;

export function getRenderer(
{
query,
Expand Down
Loading

0 comments on commit f586941

Please sign in to comment.