diff --git a/change/@nova-examples-2a4cbbef-025b-4b85-a815-97cb940d994e.json b/change/@nova-examples-2a4cbbef-025b-4b85-a815-97cb940d994e.json new file mode 100644 index 0000000..52180fb --- /dev/null +++ b/change/@nova-examples-2a4cbbef-025b-4b85-a815-97cb940d994e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "showcase pure relay examples", + "packageName": "@nova/examples", + "email": "Stanislaw.Wilczynski@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@nova-react-test-utils-7f1fc43c-3577-4483-97c6-258ed0c619c8.json b/change/@nova-react-test-utils-7f1fc43c-3577-4483-97c6-258ed0c619c8.json new file mode 100644 index 0000000..d282abb --- /dev/null +++ b/change/@nova-react-test-utils-7f1fc43c-3577-4483-97c6-258ed0c619c8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "adjust types to work well with relay graphql tag", + "packageName": "@nova/react-test-utils", + "email": "Stanislaw.Wilczynski@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/examples/package.json b/packages/examples/package.json index 259e46f..f4a1e30 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -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", @@ -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", diff --git a/packages/examples/src/apollo/Feedback/FeedbackContainer.stories.ts b/packages/examples/src/apollo/Feedback/FeedbackContainer.stories.ts index 4e1f340..991b655 100644 --- a/packages/examples/src/apollo/Feedback/FeedbackContainer.stories.ts +++ b/packages/examples/src/apollo/Feedback/FeedbackContainer.stories.ts @@ -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"); }, }; diff --git a/packages/examples/src/relay/Feedback/FeedbackContainer.stories.ts b/packages/examples/src/relay/Feedback/FeedbackContainer.stories.ts index f4fa32d..1a4fbab 100644 --- a/packages/examples/src/relay/Feedback/FeedbackContainer.stories.ts +++ b/packages/examples/src/relay/Feedback/FeedbackContainer.stories.ts @@ -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"); }, }; diff --git a/packages/examples/src/relay/pure-relay/Feedback.stories.tsx b/packages/examples/src/relay/pure-relay/Feedback.stories.tsx new file mode 100644 index 0000000..d493054 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/Feedback.stories.tsx @@ -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, +} satisfies Meta; + +export default meta; +type Story = StoryObj>; + +export const AutoGeneratedDataOnly: Story = {}; + +export const Primary: Story = { + parameters: { + novaEnvironment: { + resolvers: { + Feedback: () => sampleFeedback, + }, + }, + } satisfies WithNovaEnvironment, +}; + +export const Liked: Story = { + parameters: { + novaEnvironment: { + resolvers: { + Feedback: () => ({ + ...sampleFeedback, + doesViewerLike: true, + }), + }, + }, + } satisfies WithNovaEnvironment, +}; + +const likeResolvers = { + Feedback: () => sampleFeedback, + FeedbackLikeMutationResult: () => ({ + feedback: { + ...sampleFeedback, + doesViewerLike: true, + }, + }), +}; + +export const Like: Story = { + parameters: { + novaEnvironment: { + resolvers: likeResolvers, + }, + } satisfies WithNovaEnvironment, + 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, + 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, + 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 ( + + 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 */} + + + + Are you sure you want to delete feedback "{text}" + + + ); +}; + +export const WithDeleteDialog: Story = { + ...Primary, + render: (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, +}; diff --git a/packages/examples/src/relay/pure-relay/Feedback.test.tsx b/packages/examples/src/relay/pure-relay/Feedback.test.tsx new file mode 100644 index 0000000..3e9de4e --- /dev/null +++ b/packages/examples/src/relay/pure-relay/Feedback.test.tsx @@ -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(); + 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( + , + ); + const context = prepareStoryContextForTest( + ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError, + container, + ); + expect(async () => { + await executePlayFunction( + ArtificialFailureToShowcaseDecoratorBehaviorInCaseOfADevCausedError, + context, + ); + }).rejects.toThrowError("Query failed"); + }); +}); diff --git a/packages/examples/src/relay/pure-relay/Feedback.tsx b/packages/examples/src/relay/pure-relay/Feedback.tsx new file mode 100644 index 0000000..60bb132 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/Feedback.tsx @@ -0,0 +1,119 @@ +import { graphql, useFragment, useMutation } from "react-relay"; +import { useNovaEventing } from "@nova/react"; +import * as React from "react"; +import type { FeedbackComponent_RelayLikeMutation } from "./__generated__/FeedbackComponent_RelayLikeMutation.graphql"; +import type { Feedback_feedbackRelayFragment$key } from "./__generated__/Feedback_feedbackRelayFragment.graphql"; +import { events } from "../../events/events"; + +type Props = { + feedback: Feedback_feedbackRelayFragment$key; +}; + +export const Feedback_feedbackRelayFragment = graphql` + fragment Feedback_feedbackRelayFragment on Feedback { + id + message { + text + } + doesViewerLike + } +`; + +export const FeedbackComponent = (props: Props) => { + const [errorMessage, setErrorMessage] = React.useState(null); + const feedback = useFragment(Feedback_feedbackRelayFragment, props.feedback); + const [like, isPending] = useMutation( + graphql` + mutation FeedbackComponent_RelayLikeMutation($input: FeedbackLikeInput!) { + feedbackLike(input: $input) { + feedback { + id + doesViewerLike + } + } + } + `, + ); + + const onDeleteFeedback = useOnDeleteFeedback( + feedback.id, + feedback.message.text, + ); + + const feedbackTelemetry = useFeedbackTelemetry(); + + return ( +
+ {errorMessage != null && ( +
{errorMessage}
+ )} + Feedback: {feedback.message.text} + + +
+ ); +}; + +const useOnDeleteFeedback = (feedbackId: string, feedbackText: string) => { + const eventing = useNovaEventing(); + + return React.useCallback( + (reactEvent: React.SyntheticEvent) => { + const event = events.onDeleteFeedback({ feedbackId, feedbackText }); + void eventing.bubble({ event, reactEvent }); + }, + [eventing, feedbackId, feedbackText], + ); +}; + +const useFeedbackTelemetry = () => { + const eventing = useNovaEventing(); + + return React.useCallback( + (operation: "FeedbackLiked" | "FeedbackUnliked") => { + const event = events.feedbackTelemetry({ operation }); + void eventing.generateEvent({ event }); + }, + [eventing], + ); +}; diff --git a/packages/examples/src/relay/pure-relay/__generated__/FeedbackComponent_RelayLikeMutation.graphql.ts b/packages/examples/src/relay/pure-relay/__generated__/FeedbackComponent_RelayLikeMutation.graphql.ts new file mode 100644 index 0000000..95a6d97 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/__generated__/FeedbackComponent_RelayLikeMutation.graphql.ts @@ -0,0 +1,114 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Mutation } from 'relay-runtime'; +export type FeedbackLikeInput = { + doesViewerLike: boolean; + feedbackId: string; +}; +export type FeedbackComponent_RelayLikeMutation$variables = { + input: FeedbackLikeInput; +}; +export type FeedbackComponent_RelayLikeMutation$data = { + readonly feedbackLike: { + readonly feedback: { + readonly doesViewerLike: boolean; + readonly id: string; + }; + }; +}; +export type FeedbackComponent_RelayLikeMutation = { + response: FeedbackComponent_RelayLikeMutation$data; + variables: FeedbackComponent_RelayLikeMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "FeedbackLikeMutationResult", + "kind": "LinkedField", + "name": "feedbackLike", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Feedback", + "kind": "LinkedField", + "name": "feedback", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "doesViewerLike", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FeedbackComponent_RelayLikeMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FeedbackComponent_RelayLikeMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "968ac2c311a93d7c192457e711271381", + "id": null, + "metadata": {}, + "name": "FeedbackComponent_RelayLikeMutation", + "operationKind": "mutation", + "text": "mutation FeedbackComponent_RelayLikeMutation(\n $input: FeedbackLikeInput!\n) {\n feedbackLike(input: $input) {\n feedback {\n id\n doesViewerLike\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "64a255c318ab3d381ce8b70966fd7c6b"; + +export default node; diff --git a/packages/examples/src/relay/pure-relay/__generated__/FeedbackContainerRelayQuery.graphql.ts b/packages/examples/src/relay/pure-relay/__generated__/FeedbackContainerRelayQuery.graphql.ts new file mode 100644 index 0000000..63582b7 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/__generated__/FeedbackContainerRelayQuery.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FeedbackContainerRelayQuery$variables = { + id: string; +}; +export type FeedbackContainerRelayQuery$data = { + readonly feedback: { + readonly " $fragmentSpreads": FragmentRefs<"Feedback_feedbackRelayFragment">; + }; +}; +export type FeedbackContainerRelayQuery = { + response: FeedbackContainerRelayQuery$data; + variables: FeedbackContainerRelayQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FeedbackContainerRelayQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "Feedback", + "kind": "LinkedField", + "name": "feedback", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "Feedback_feedbackRelayFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FeedbackContainerRelayQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "Feedback", + "kind": "LinkedField", + "name": "feedback", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Message", + "kind": "LinkedField", + "name": "message", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "text", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "doesViewerLike", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "c09a01af0895b3a83ec47cb4908d47ad", + "id": null, + "metadata": {}, + "name": "FeedbackContainerRelayQuery", + "operationKind": "query", + "text": "query FeedbackContainerRelayQuery(\n $id: ID!\n) {\n feedback(id: $id) {\n ...Feedback_feedbackRelayFragment\n id\n }\n}\n\nfragment Feedback_feedbackRelayFragment on Feedback {\n id\n message {\n text\n }\n doesViewerLike\n}\n" + } +}; +})(); + +(node as any).hash = "6c5330e7b7e1af00327c1eb5c7bf5fa0"; + +export default node; diff --git a/packages/examples/src/relay/pure-relay/__generated__/FeedbackStoryRelayQuery.graphql.ts b/packages/examples/src/relay/pure-relay/__generated__/FeedbackStoryRelayQuery.graphql.ts new file mode 100644 index 0000000..b58b020 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/__generated__/FeedbackStoryRelayQuery.graphql.ts @@ -0,0 +1,165 @@ +/** + * @generated SignedSource<<8ca401641ed05d5f1f3a338005137520>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FeedbackStoryRelayQuery$variables = { + id: string; +}; +export type FeedbackStoryRelayQuery$data = { + readonly feedback: { + readonly " $fragmentSpreads": FragmentRefs<"Feedback_feedbackRelayFragment">; + }; +}; +export type FeedbackStoryRelayQuery = { + response: FeedbackStoryRelayQuery$data; + variables: FeedbackStoryRelayQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FeedbackStoryRelayQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "Feedback", + "kind": "LinkedField", + "name": "feedback", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "Feedback_feedbackRelayFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FeedbackStoryRelayQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "Feedback", + "kind": "LinkedField", + "name": "feedback", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Message", + "kind": "LinkedField", + "name": "message", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "text", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "doesViewerLike", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3a00fb452776d7d61323755d1f81e58f", + "id": null, + "metadata": { + "relayTestingSelectionTypeInfo": { + "feedback": { + "enumValues": null, + "nullable": false, + "plural": false, + "type": "Feedback" + }, + "feedback.doesViewerLike": { + "enumValues": null, + "nullable": false, + "plural": false, + "type": "Boolean" + }, + "feedback.id": { + "enumValues": null, + "nullable": false, + "plural": false, + "type": "ID" + }, + "feedback.message": { + "enumValues": null, + "nullable": false, + "plural": false, + "type": "Message" + }, + "feedback.message.text": { + "enumValues": null, + "nullable": false, + "plural": false, + "type": "String" + } + } + }, + "name": "FeedbackStoryRelayQuery", + "operationKind": "query", + "text": "query FeedbackStoryRelayQuery(\n $id: ID!\n) {\n feedback(id: $id) {\n ...Feedback_feedbackRelayFragment\n id\n }\n}\n\nfragment Feedback_feedbackRelayFragment on Feedback {\n id\n message {\n text\n }\n doesViewerLike\n}\n" + } +}; +})(); + +(node as any).hash = "6089c0fe5f98406204254d9b33a6e061"; + +export default node; diff --git a/packages/examples/src/relay/pure-relay/__generated__/Feedback_feedbackRelayFragment.graphql.ts b/packages/examples/src/relay/pure-relay/__generated__/Feedback_feedbackRelayFragment.graphql.ts new file mode 100644 index 0000000..b21abd1 --- /dev/null +++ b/packages/examples/src/relay/pure-relay/__generated__/Feedback_feedbackRelayFragment.graphql.ts @@ -0,0 +1,71 @@ +/** + * @generated SignedSource<<857e369af95fed51d17a8de7c72bbc62>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { Fragment, ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type Feedback_feedbackRelayFragment$data = { + readonly doesViewerLike: boolean; + readonly id: string; + readonly message: { + readonly text: string; + }; + readonly " $fragmentType": "Feedback_feedbackRelayFragment"; +}; +export type Feedback_feedbackRelayFragment$key = { + readonly " $data"?: Feedback_feedbackRelayFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"Feedback_feedbackRelayFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "Feedback_feedbackRelayFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Message", + "kind": "LinkedField", + "name": "message", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "text", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "doesViewerLike", + "storageKey": null + } + ], + "type": "Feedback", + "abstractKey": null +}; + +(node as any).hash = "489883dd98e3f989898f778ed38f10ea"; + +export default node; diff --git a/packages/nova-react-test-utils/README.md b/packages/nova-react-test-utils/README.md index 8adac91..1a5519d 100644 --- a/packages/nova-react-test-utils/README.md +++ b/packages/nova-react-test-utils/README.md @@ -214,6 +214,10 @@ It is helpful if your event changes something on integration side of your compon ## FAQ +#### I am not using Nova directly, but I am using Relay. Can I still use this package? + +Yes, you can. The Storybook decorator is designed to work with any Relay component, not just those that use Nova. The only change is that your components use Relay hooks for data fetching and instead of import `graphql` from `@nova/react` you can use `graphql` from `react-relay`. Check [the pure Relay examples](../examples/src/relay/pure-relay/Feedback.tsx). + #### I need to configure cache of the Apollo mock client as I am using @graphitation/apollo-react-relay-duct-tape together with watch fragments that rely on bein able to fetch data from cache. Is it configurable? Yes, if you are using through unit tests directly you can pass options to `createMockEnvironment`: diff --git a/packages/nova-react-test-utils/src/shared/storybook-nova-decorator-shared.tsx b/packages/nova-react-test-utils/src/shared/storybook-nova-decorator-shared.tsx index de1f00d..642a1ec 100644 --- a/packages/nova-react-test-utils/src/shared/storybook-nova-decorator-shared.tsx +++ b/packages/nova-react-test-utils/src/shared/storybook-nova-decorator-shared.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { type GraphQLTaggedNode, useLazyLoadQuery } from "@nova/react"; +import { type GraphQLTaggedNode as RelayGraphQLTaggedNode } from "relay-runtime"; import type { MockResolvers } from "@graphitation/graphql-js-operation-payload-generator"; import type { Addon_LegacyStoryFn, @@ -37,7 +38,7 @@ export type WithNovaEnvironment< novaEnvironment: | ( | { - query: GraphQLTaggedNode; + query: GraphQLTaggedNode | RelayGraphQLTaggedNode; variables?: TQuery["variables"]; referenceEntries: Record< string, @@ -82,7 +83,11 @@ export function getRenderer( ): React.FC> { if (query) { const Renderer: React.FC = () => { - const { data } = useLazyLoadQuery(query, variables); + const { data } = useLazyLoadQuery( + // There are no consequences of the cast, we do it only to make sure pure relay components can also leverage the decorator + query as GraphQLTaggedNode, + variables, + ); // apollo does not suspend, but returns undefined data if (!data) {