Skip to content

Commit

Permalink
major(nova-react-test-utils): rewrite EventingProvider to be Eventing…
Browse files Browse the repository at this point in the history
…Interceptor (#131)

* use interceptor instead of eventing

* Change files

* fix stories

---------

Co-authored-by: Stanislaw Wilczynski <[email protected]>
  • Loading branch information
sjwilczynski and Stanislaw Wilczynski authored Jan 14, 2025
1 parent a8ccff2 commit 6dfd060
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "use interceptor instead of eventing",
"packageName": "@nova/examples",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "rewrite evetingprovider to eventinginterceptor",
"packageName": "@nova/react-test-utils",
"email": "[email protected]",
"dependentChangeType": "patch"
}
14 changes: 10 additions & 4 deletions packages/examples/src/apollo/Feedback/Feedback.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { graphql } from "@nova/react";
import {
EventingProvider,
EventingInterceptor,
getNovaDecorator,
getNovaEnvironmentForStory,
type WithNovaEnvironment,
Expand Down Expand Up @@ -82,6 +82,8 @@ export const Like: Story = {
const container = within(context.canvasElement);
const likeButton = await container.findByRole("button", { name: "Like" });
await userEvent.click(likeButton);

await container.findByRole("button", { name: "Unlike" });
},
};

Expand Down Expand Up @@ -110,12 +112,16 @@ const FeedbackWithDeleteDialog = (
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
<EventingProvider<typeof events>
<EventingInterceptor<typeof events>
eventMap={{
onDeleteFeedback: (eventWrapper) => {
setOpen(true);
setText(eventWrapper.event.data().feedbackText);
return Promise.resolve();
return Promise.resolve(undefined);
},
feedbackTelemetry: (eventWrapper) => {
console.log("Telemetry event", eventWrapper.event.data());
return Promise.resolve(eventWrapper);
},
}}
>
Expand All @@ -125,7 +131,7 @@ const FeedbackWithDeleteDialog = (
<button onClick={() => setOpen(false)}>Cancel</button>
Are you sure you want to delete feedback "{text}"
</dialog>
</EventingProvider>
</EventingInterceptor>
);
};

Expand Down
15 changes: 10 additions & 5 deletions packages/examples/src/relay/Feedback/Feedback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MockPayloadGenerator as PayloadGenerator,
type StoryObjWithoutFragmentRefs,
type WithNovaEnvironment,
EventingProvider,
EventingInterceptor,
} from "@nova/react-test-utils/relay";
import type { Meta } from "@storybook/react";
import { userEvent, waitFor, within, expect } from "@storybook/test";
Expand Down Expand Up @@ -104,9 +104,10 @@ export const Like: Story = {
const operation = mock.getMostRecentOperation();
await expect(operation).toBeDefined();
});
await mock.resolveMostRecentOperation((operation) => {
mock.resolveMostRecentOperation((operation) => {
return MockPayloadGenerator.generate(operation, likeResolvers);
});
await container.findByRole("button", { name: "Unlike" });
},
};

Expand Down Expand Up @@ -135,12 +136,16 @@ const FeedbackWithDeleteDialog = (
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
<EventingProvider<typeof events>
<EventingInterceptor<typeof events>
eventMap={{
onDeleteFeedback: (eventWrapper) => {
setOpen(true);
setText(eventWrapper.event.data().feedbackText);
return Promise.resolve();
return Promise.resolve(undefined);
},
feedbackTelemetry: (eventWrapper) => {
console.log("Telemetry event", eventWrapper.event.data());
return Promise.resolve(eventWrapper);
},
}}
>
Expand All @@ -149,7 +154,7 @@ const FeedbackWithDeleteDialog = (
<button onClick={() => setOpen(false)}>Cancel</button>
Are you sure you want to delete feedback "{text}"
</dialog>
</EventingProvider>
</EventingInterceptor>
);
};

Expand Down
20 changes: 10 additions & 10 deletions packages/examples/src/relay/pure-relay/Feedback.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
getNovaDecorator,
getNovaEnvironmentForStory,
type WithNovaEnvironment,
EventingProvider,
EventingInterceptor,
getOperationName,
getOperationType,
type StoryObjWithoutFragmentRefs,
Expand Down Expand Up @@ -98,7 +98,6 @@ export const Liked: Story = {
};

const likeResolvers = {
Feedback: () => sampleFeedback,
FeedbackLikeMutationResult: () => ({
feedback: {
...sampleFeedback,
Expand All @@ -108,11 +107,7 @@ const likeResolvers = {
};

export const Like: Story = {
parameters: {
novaEnvironment: {
resolvers: likeResolvers,
},
} satisfies WithNovaEnvironment<FeedbackStoryRelayQuery, TypeMap>,
parameters: Primary.parameters,
play: async (context) => {
const container = within(context.canvasElement);
const likeButton = await container.findByRole("button", { name: "Like" });
Expand All @@ -129,6 +124,7 @@ export const Like: Story = {
mock.resolveMostRecentOperation((operation) => {
return MockPayloadGenerator.generate(operation, likeResolvers);
});
await container.findByRole("button", { name: "Unlike" });
},
};

Expand Down Expand Up @@ -203,12 +199,16 @@ const FeedbackWithDeleteDialog = (
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
<EventingProvider<typeof events>
<EventingInterceptor<typeof events>
eventMap={{
onDeleteFeedback: (eventWrapper) => {
setOpen(true);
setText(eventWrapper.event.data().feedbackText);
return Promise.resolve();
return Promise.resolve(undefined);
},
feedbackTelemetry: (eventWrapper) => {
console.log("Telemetry event", eventWrapper.event.data());
return Promise.resolve(eventWrapper);
},
}}
>
Expand All @@ -217,7 +217,7 @@ const FeedbackWithDeleteDialog = (
<button onClick={() => setOpen(false)}>Cancel</button>
Are you sure you want to delete feedback "{text}"
</dialog>
</EventingProvider>
</EventingInterceptor>
);
};

Expand Down
14 changes: 9 additions & 5 deletions packages/nova-react-test-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ You can also see that `satisfies WithNovaEnvironment<Operation, TypeMap>` is use

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
### EventingInterceptor

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:

Expand All @@ -188,13 +188,17 @@ const FeedbackWithDeleteDialog = (
const [open, setOpen] = React.useState(false);
const [text, setText] = React.useState("");
return (
<EventingProvider<typeof events> // this should be an events object that has definitions for all your events for good TS auto-completion
<EventingInterceptor<typeof events> // this should be an events object that has definitions for all your events for good TS auto-completion
eventMap={{
onDeleteFeedback: (eventWrapper) => {
// custom handler for this specific event
setOpen(true);
setText(eventWrapper.event.data().feedbackText);
return Promise.resolve();
return Promise.resolve(undefined); // return undefined if you want to stop processing event further
},
feedbackTelemetry: (eventWrapper) => {
console.log("Telemetry event", eventWrapper.event.data());
return Promise.resolve(eventWrapper); // return eventWrapper if you want to pass event further, for default bubble to log it in actions tab
},
}}
>
Expand All @@ -203,7 +207,7 @@ const FeedbackWithDeleteDialog = (
<button onClick={() => setOpen(false)}>Cancel</button>
Are you sure you want to delete feedback "{text}"
</dialog>
</EventingProvider>
</EventingInterceptor>
);
};

Expand All @@ -221,7 +225,7 @@ export const WithDeleteDialog: Story = {
};
```

It is helpful if your event changes something on integration side of your component and you want simulate that behavior in Storybook.
It is helpful if your event changes something on integration side of your component and you want simulate that behavior in Storybook. Implementation wise `EventingInterceptor` uses [eventing interceptor](../nova-react/README.md#intercepting-events) under the hood, so that you can always use the default handler if needed.

## FAQ

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 @@ -7,7 +7,7 @@ export type {
MockResolvers,
DefaultMockResolvers,
} from "../shared/storybook-nova-decorator-shared";
export { EventingProvider } from "../shared/eventing-provider";
export { EventingInterceptor } from "../shared/eventing-interceptor";
export type { StoryObjWithoutFragmentRefs } from "../shared/types";

export {
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 @@ -7,7 +7,7 @@ export type {
MockResolvers,
DefaultMockResolvers,
} from "../shared/storybook-nova-decorator-shared";
export { EventingProvider } from "../shared/eventing-provider";
export { EventingInterceptor } from "../shared/eventing-interceptor";
export type { StoryObjWithoutFragmentRefs } from "../shared/types";

export {
Expand Down
41 changes: 41 additions & 0 deletions packages/nova-react-test-utils/src/shared/eventing-interceptor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NovaEventingInterceptor } from "@nova/react";
import type { EventWrapper, NovaEvent, Source } from "@nova/types";
import * as React from "react";

type EventCreatorMap = Record<string, (...args: any[]) => NovaEvent<unknown>>;
type EventMap<T extends EventCreatorMap> = {
[Property in keyof T]?: (eventWrapper: {
event: ReturnType<T[Property]>;
source: Source;
}) => Promise<undefined | EventWrapper>;
};

export const EventingInterceptor = <T extends EventCreatorMap>({
eventMap,
children,
}: {
eventMap: EventMap<T>;
children: React.ReactNode;
}) => {
const interceptor = (eventWrapper: EventWrapper) => {
const eventType = eventWrapper.event.type;
const customEventHandler = eventMap[eventType];
if (customEventHandler) {
return customEventHandler(
// As the key was in the map we now the type is correct
eventWrapper as unknown as {
event: ReturnType<T[keyof T]>;
source: Source;
},
);
} else {
return Promise.resolve(eventWrapper);
}
};

return (
<NovaEventingInterceptor interceptor={interceptor}>
{children}
</NovaEventingInterceptor>
);
};
40 changes: 0 additions & 40 deletions packages/nova-react-test-utils/src/shared/eventing-provider.tsx

This file was deleted.

0 comments on commit 6dfd060

Please sign in to comment.