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): add storybook decorator for nova #72

Merged
merged 31 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
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
7 changes: 2 additions & 5 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"es6": true,
"node": true
},
"ignorePatterns": ["**/coverage", "**/lib", "**/temp"],
"ignorePatterns": ["**/coverage", "**/lib", "**/temp", "**/__generated__"],
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/consistent-type-imports": [
Expand All @@ -20,8 +20,5 @@
}
]
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier"
]
"extends": ["plugin:@typescript-eslint/recommended", "prettier"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "add examples package",
"packageName": "@nova/examples",
"email": "Stanislaw.Wilczynski@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 7 additions & 0 deletions change/@nova-react-38a57d03-7f69-4197-b2f2-4ba427896bb8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "bump version of relay-compiler-language-graphitation",
"packageName": "@nova/react",
"email": "Stanislaw.Wilczynski@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "add storybook decorator for nova components",
"packageName": "@nova/react-test-utils",
"email": "Stanislaw.Wilczynski@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 7 additions & 0 deletions change/@nova-types-3378b7b7-3f6f-45d5-82ce-687385f75b70.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "bump deps",
"packageName": "@nova/types",
"email": "Stanislaw.Wilczynski@microsoft.com",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions lage.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
pipeline: {
prepare: [],
types: ["^types"],
build: [],
test: ["build"],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "lage test",
"lint": "lage lint",
"lage": "lage",
"prepare": "lage prepare",
"ci": "yarn lage build types test lint && yarn checkchange",
"beachball": "beachball -b origin/main",
"change": "yarn beachball change",
Expand Down
4 changes: 4 additions & 0 deletions packages/examples/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["../../.eslintrc.json"],
"root": true
}
42 changes: 42 additions & 0 deletions packages/examples/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { StorybookConfig } from "@storybook/react-webpack5";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-webpack5",
options: {},
},
webpackFinal: (config) => {
return {
...config,
module: {
...config.module,
rules: [
...(config?.module?.rules ?? []),
{
test: /\.(tsx|ts)$/,
use: [
{
loader: require.resolve("esbuild-loader"),
options: {
loader: "tsx",
target: "es2020",
},
},
],
exclude: /node_modules/,
},
{
test: /\.(graphql)$/,
loader: "@graphql-tools/webpack-loader",
},
],
},
};
},
};
export default config;
5 changes: 5 additions & 0 deletions packages/examples/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Preview } from "@storybook/react";

const preview: Preview = {};

export default preview;
alloy marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions packages/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Nova examples

This package contains examples of nova components as well as related good testing approaches for them.
23 changes: 23 additions & 0 deletions packages/examples/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
overwrite: true
schema:
- "./src/schema.graphql"
generates:
src/__generated__/schema.all.interface.ts:
plugins:
- add:
content:
- // THIS FILE WAS AUTOMATICALLY GENERATED AND SHOULD NOT BE EDITED
-
- "typescript"
- "typescript-resolvers"
- "@graphitation/graphql-codegen-typescript-typemap-plugin"

config:
avoidOptionals: true
preResolveTypes: true
skipTypeNameForRoot: true
exportFragmentSpreadSubTypes: true
# skipTypename: true
useTypeImports: true
allowParentTypeOverride: true
enumsAsTypes: true
56 changes: 56 additions & 0 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@nova/examples",
"license": "MIT",
"version": "0.0.1",
"main": "./src/index.ts",
"scripts": {
"build": "monorepo-scripts build",
"generate": "graphql-codegen --config codegen.yml && nova-graphql-compiler --schema ./src/schema.graphql --src ./src --watchman false",
"lint": "monorepo-scripts lint",
"prepare": "yarn generate",
"test": "monorepo-scripts test",
"types": "monorepo-scripts types",
"just": "monorepo-scripts",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@nova/react": "1.2.1",
"@nova/react-test-utils": "3.1.3",
"@nova/types": "1.3.0",
"graphql": "^15.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@graphitation/graphql-codegen-typescript-typemap-plugin": "^0.1.5",
"@graphitation/graphql-js-tag": "^0.9.0",
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/cli": "^3.2.2",
"@graphql-codegen/typescript": "^3.0.2",
"@graphql-codegen/typescript-resolvers": "^3.1.1",
"@graphql-tools/webpack-loader": "^6.7.1",
"@storybook/addon-essentials": "^7.0.11",
"@storybook/addon-interactions": "^7.0.11",
"@storybook/addon-links": "^7.0.11",
"@storybook/blocks": "^7.0.11",
"@storybook/react": "^7.0.11",
"@storybook/react-webpack5": "^7.0.11",
"@storybook/testing-library": "^0.1.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^29.2.0",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.11",
"esbuild-loader": "^3.0.1",
"monorepo-scripts": "*",
"prop-types": "15.8.1",
"storybook": "^7.0.11",
"typescript": ">=4.9.5"
},
"sideEffects": false
}
69 changes: 69 additions & 0 deletions packages/examples/src/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { graphql, useFragment, useMutation } from "@nova/react";
import * as React from "react";
import type { FeedbackComponent_LikeMutation } from "./__generated__/FeedbackComponent_LikeMutation.graphql";
import type { Feedback_feedbackFragment$key } from "./__generated__/Feedback_feedbackFragment.graphql";

type Props = {
feedback: Feedback_feedbackFragment$key;
};

export const Feedback_feedbackFragment = graphql`
fragment Feedback_feedbackFragment on Feedback {
id
message {
text
}
doesViewerLike
}
`;

export const FeedbackComponent = (props: Props) => {
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const feedback = useFragment(Feedback_feedbackFragment, props.feedback);
const [like, isPending] = useMutation<FeedbackComponent_LikeMutation>(
graphql`
mutation FeedbackComponent_LikeMutation($input: FeedbackLikeInput!) {
feedbackLike(input: $input) {
feedback {
id
doesViewerLike
}
}
}
`,
);
return (
<div>
{errorMessage != null && (
<div style={{ color: "red" }}>{errorMessage}</div>
)}
Feedback: {feedback.message.text}
<button
id="likeButton"
disabled={isPending}
onClick={() => {
like({
variables: {
input: {
feedbackId: feedback.id,
doesViewerLike: !feedback.doesViewerLike,
},
},
optimisticResponse: {
feedbackLike: {
feedback: {
id: feedback.id,
doesViewerLike: !feedback.doesViewerLike,
},
},
},
}).catch(() => {
setErrorMessage("Something went wrong");
});
}}
>
{feedback.doesViewerLike ? "Unlike" : "Like"}
</button>
</div>
);
};
104 changes: 104 additions & 0 deletions packages/examples/src/Feedback/FeedbackContainer.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/testing-library";
import { FeedbackContainer } from "./FeedbackContainer";
import type { NovaEnvironmentDecoratorParameters } from "@nova/react-test-utils";
import {
getNovaEnvironmentForStory,
getNovaEnvironmentDecorator,
MockPayloadGenerator,
} from "@nova/react-test-utils";
import { getSchema } from "../testing-utils/getSchema";
import type { TypeMap } from "../__generated__/schema.all.interface";

const schema = getSchema();

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

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

export const Primary: Story = {
parameters: {
novaEnvironment: {
resolvers: {
Feedback: () => sampleFeedback,
},
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if we can get rid of this in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of satisfies? Then probably no, we won't get rid of it. We still need a way to strongly type parameters (by default it's just generic Record) so we either need to use satisfies or adjust story type itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the satisfies part. I figured this was the reason, but I’m thinking that it should be very possible to explain users how they can create their own StoryObj type that composed the normal StoryObj and a resolvers map with their schema.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(To be clear, this is not a blocker!)

Copy link
Contributor Author

@sjwilczynski sjwilczynski May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can always do const Primary: Story & {parameters: NovaEnvironmentDecoratorParameters<TypeMap>} but I personally like satisfies part :)

};

export const Liked: Story = {
parameters: {
novaEnvironment: {
resolvers: {
Feedback: () => ({
...sampleFeedback,
doesViewerLike: true,
}),
},
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
};

export const Like: Story = {
parameters: {
novaEnvironment: {
resolvers: {
Feedback: () => sampleFeedback,
FeedbackLikeMutationResult: () => ({
feedback: {
...sampleFeedback,
doesViewerLike: true,
},
}),
},
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
play: async ({ canvasElement }) => {
const container = within(canvasElement);
const likeButton = await container.findByRole("button", { name: "Like" });
userEvent.click(likeButton);
},
};

export const LikeFailure: Story = {
parameters: {
novaEnvironment: {
enableQueuedMockResolvers: false,
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
play: async (context) => {
const {
graphql: { mock },
} = getNovaEnvironmentForStory(context);

// wait for next tick for apollo client to update state
await new Promise((resolve) => setTimeout(resolve, 0));
await mock.resolveMostRecentOperation((operation) =>
MockPayloadGenerator.generate(operation, {
Feedback: () => sampleFeedback,
}),
);
await Like.play?.(context);
mock.rejectMostRecentOperation(new Error("Like failed"));
},
};

export const Loading: Story = {
parameters: {
novaEnvironment: {
enableQueuedMockResolvers: false,
},
} satisfies NovaEnvironmentDecoratorParameters<TypeMap>,
};

const sampleFeedback = {
id: "42",
message: {
text: "Feedback title",
},
doesViewerLike: false,
};
Loading