Skip to content

Commit

Permalink
fix(nova-react-test-utils): adjust types to properly support pure Rel…
Browse files Browse the repository at this point in the history
…ay components (#113)

* support relay

* Change files

---------

Co-authored-by: Stanislaw Wilczynski <[email protected]>
  • Loading branch information
sjwilczynski and Stanislaw Wilczynski authored Sep 19, 2024
1 parent 02451e6 commit 5199008
Show file tree
Hide file tree
Showing 14 changed files with 884 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "showcase pure relay examples",
"packageName": "@nova/examples",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "adjust types to work well with relay graphql tag",
"packageName": "@nova/react-test-utils",
"email": "[email protected]",
"dependentChangeType": "patch"
}
4 changes: 3 additions & 1 deletion packages/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@nova/types": "1.5.1",
"graphql": "^15.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-relay": "^17.0.0"
},
"devDependencies": {
"@babel/core": "^7.20.2",
Expand Down Expand Up @@ -51,6 +52,7 @@
"@types/jest": "^29.2.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/react-relay": "^16.0.0",
"esbuild-loader": "^3.0.1",
"monorepo-scripts": "*",
"prop-types": "15.8.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export const LikeFailure: Story = {
expect(operation).toBeDefined();
});
mock.rejectMostRecentOperation(new Error("Like failed"));
const container = within(context.canvasElement);
await container.findByText("Something went wrong");
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const LikeFailure: Story = {
expect(nextOperationName).toEqual("FeedbackComponent_LikeMutation");
expect(nextOperationType).toEqual("mutation");
await mock.rejectMostRecentOperation(new Error("Like failed"));
await container.findByText("Something went wrong");
},
};

Expand Down
213 changes: 213 additions & 0 deletions packages/examples/src/relay/pure-relay/Feedback.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { graphql } from "react-relay";
import {
getNovaDecorator,
getNovaEnvironmentForStory,
MockPayloadGenerator as PayloadGenerator,
type WithoutFragmentRefs,
type WithNovaEnvironment,
EventingProvider,
getOperationName,
getOperationType,
} from "@nova/react-test-utils/relay";
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, waitFor, within, expect } from "@storybook/test";
import type { TypeMap } from "../../__generated__/schema.all.interface";
import { FeedbackComponent } from "./Feedback";
import type { FeedbackStoryRelayQuery } from "./__generated__/FeedbackStoryRelayQuery.graphql";
import { getSchema } from "../../testing-utils/getSchema";
import * as React from "react";
import type { events } from "../../events/events";

const schema = getSchema();

const MockPayloadGenerator = new PayloadGenerator(schema);

const meta = {
component: FeedbackComponent,
decorators: [getNovaDecorator(schema)],
parameters: {
novaEnvironment: {
query: graphql`
query FeedbackStoryRelayQuery($id: ID!) @relay_test_operation {
feedback(id: $id) {
...Feedback_feedbackRelayFragment
}
}
`,
variables: { id: "42" },
referenceEntries: {
feedback: (data) => data?.feedback,
},
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
} satisfies Meta<typeof FeedbackComponent>;

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

export const AutoGeneratedDataOnly: Story = {};

export const Primary: Story = {
parameters: {
novaEnvironment: {
resolvers: {
Feedback: () => sampleFeedback,
},
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
};

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

const likeResolvers = {
Feedback: () => sampleFeedback,
FeedbackLikeMutationResult: () => ({
feedback: {
...sampleFeedback,
doesViewerLike: true,
},
}),
};

export const Like: Story = {
parameters: {
novaEnvironment: {
resolvers: likeResolvers,
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
play: async (context) => {
const container = within(context.canvasElement);
const likeButton = await container.findByRole("button", { name: "Like" });
await userEvent.click(likeButton);

const {
graphql: { mock },
} = getNovaEnvironmentForStory(context);

await waitFor(async () => {
const operation = mock.getMostRecentOperation();
await expect(operation).toBeDefined();
});
await mock.resolveMostRecentOperation((operation) => {
return MockPayloadGenerator.generate(operation, likeResolvers);
});
},
};

export const ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError: Story =
{
parameters: {
novaEnvironment: {
enableQueuedMockResolvers: false,
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
play: async (context) => {
const {
graphql: { mock },
} = getNovaEnvironmentForStory(context);
await waitFor(async () => {
const operation = mock.getMostRecentOperation();
await expect(operation).toBeDefined();
});
await mock.rejectMostRecentOperation(new Error("Query failed"));
},
};

export const LikeFailure: Story = {
parameters: {
novaEnvironment: {
enableQueuedMockResolvers: false,
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
play: async (context) => {
const container = within(context.canvasElement);
const {
graphql: { mock },
} = getNovaEnvironmentForStory(context);

await waitFor(async () => {
const operation = mock.getMostRecentOperation();
await expect(operation).toBeDefined();
});
const operation = mock.getMostRecentOperation();
const operationName = getOperationName(operation);
const operationType = getOperationType(operation);
expect(operationName).toEqual("FeedbackStoryRelayQuery");
expect(operationType).toEqual("query");
await mock.resolveMostRecentOperation((operation) => {
return MockPayloadGenerator.generate(operation, {
Feedback: () => sampleFeedback,
});
});
const likeButton = await container.findByRole("button", { name: "Like" });
userEvent.click(likeButton);
await waitFor(async () => {
const operation = mock.getMostRecentOperation();
expect(operation).toBeDefined();
});
const nextOperation = mock.getMostRecentOperation();
const nextOperationName = getOperationName(nextOperation);
const nextOperationType = getOperationType(nextOperation);
expect(nextOperationName).toEqual("FeedbackComponent_RelayLikeMutation");
expect(nextOperationType).toEqual("mutation");
await mock.rejectMostRecentOperation(new Error("Like failed"));
await container.findByText("Something went wrong");
},
};

const FeedbackWithDeleteDialog = (props: Story["args"]) => {
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
<EventingProvider<typeof events>
eventMap={{
onDeleteFeedback: (eventWrapper) => {
setOpen(true);
setText(eventWrapper.event.data().feedbackText);
return Promise.resolve();
},
}}
>
{/* 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>
Are you sure you want to delete feedback "{text}"
</dialog>
</EventingProvider>
);
};

export const WithDeleteDialog: Story = {
...Primary,
render: (args) => <FeedbackWithDeleteDialog {...args} />,
play: async (context) => {
const container = within(context.canvasElement);
const deleteButton = await container.findByRole("button", {
name: "Delete feedback",
});
await userEvent.click(deleteButton);
const dialog = await container.findByRole("dialog");
await expect(dialog).toBeInTheDocument();
},
};

const sampleFeedback = {
id: "42",
message: {
text: "Feedback title",
},
doesViewerLike: false,
};
39 changes: 39 additions & 0 deletions packages/examples/src/relay/pure-relay/Feedback.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { composeStories } from "@storybook/react";
import * as stories from "./Feedback.stories";
import { render, screen } from "@testing-library/react";
import * as React from "react";
import "@testing-library/jest-dom";
import { executePlayFunction } from "../../testing-utils/executePlayFunction";
import { prepareStoryContextForTest } from "@nova/react-test-utils/relay";

const {
ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError,
LikeFailure,
} = composeStories(stories);

describe("Feedback", () => {
it("should show an error if the like button fails", async () => {
const { container } = render(<LikeFailure />);
await executePlayFunction(
LikeFailure,
prepareStoryContextForTest(LikeFailure, container),
);
const error = await screen.findByText("Something went wrong");
expect(error).toBeInTheDocument();
});
it("throws an error when the developer makes a mistake", async () => {
const { container } = render(
<ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError />,
);
const context = prepareStoryContextForTest(
ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError,
container,
);
expect(async () => {
await executePlayFunction(
ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError,
context,
);
}).rejects.toThrowError("Query failed");
});
});
Loading

0 comments on commit 5199008

Please sign in to comment.