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

fix(nova/react): fix eventing for multiple nova event interceptors #132

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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: 7 additions & 0 deletions change/@nova-react-198378aa-bbb8-4a24-8ce3-2d0718002e1e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix eventing pointer for multiple interceptors",
"packageName": "@nova/react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
143 changes: 143 additions & 0 deletions packages/nova-react/src/eventing/nova-eventing-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,146 @@ describe("NovaEventingInterceptor", () => {
expect(bubbleCall.event.data()).toBe("addedData");
});
});

describe("Multiple NovaEventingInterceptors", () => {
const originalError = console.error;
beforeEach(() => {
jest.clearAllMocks();
console.error = originalError;
});
const realMapper = jest.requireActual(
"./react-event-source-mapper",
).mapEventMetadata;

const mapEventMetadataMock = jest.fn().mockImplementation(realMapper);

const bubbleMock = jest.fn();

const parentEventing = {
bubble: bubbleMock,
generateEvent: bubbleMock,
} as unknown as NovaEventing;

const callbackToBeCalledOnFirstIntercept = jest.fn();
const callbackToBeCalledOnSecondIntercept = jest.fn();

const evenOriginatorToBeInterceptedFirst = "toBeInterceptedFirst";
const evenOriginatorToBeInterceptedSecond = "toBeInterceptedSecond";

const firstInterceptor = (eventWrapper: EventWrapper) => {
console.log("received event", eventWrapper.event.originator);
carmenberndt marked this conversation as resolved.
Show resolved Hide resolved
if (eventWrapper.event.originator === evenOriginatorToBeInterceptedFirst) {
callbackToBeCalledOnFirstIntercept();
return Promise.resolve(undefined);
} else {
return Promise.resolve(eventWrapper);
}
};

const secondInterceptor = (eventWrapper: EventWrapper) => {
if (eventWrapper.event.originator === evenOriginatorToBeInterceptedSecond) {
callbackToBeCalledOnSecondIntercept();
return Promise.resolve(undefined);
} else {
console.log("bubbling up event", eventWrapper.event.originator);
carmenberndt marked this conversation as resolved.
Show resolved Hide resolved
return Promise.resolve(eventWrapper);
}
};

const ComponentWithTwoEvents = ({
name,
eventOriginatorToBeIntercepted,
}: {
name: string;
eventOriginatorToBeIntercepted: string;
}) => {
const eventing = useNovaEventing();
const onInterceptClick = (event: React.SyntheticEvent) => {
eventing.bubble({
reactEvent: event,
event: {
originator: eventOriginatorToBeIntercepted,
type: "TypeToBeIntercepted",
},
});
};

const onNonInterceptClick = (event: React.SyntheticEvent) => {
eventing.bubble({
reactEvent: event,
event: {
originator: "notToBeIntercepted",
type: "TypeNotToBeIntercepted",
},
});
};
return (
<>
Component with two events
<button onClick={onInterceptClick}>
{name}: Fire event to be intercepted
</button>
<button onClick={onNonInterceptClick}>
{name}: Fire event without intercept
</button>
</>
);
};

const MultipleInterceptorsTestComponent: React.FC = () => (
<NovaEventingProvider
eventing={parentEventing}
reactEventMapper={mapEventMetadataMock}
>
<NovaEventingInterceptor interceptor={firstInterceptor}>
<NovaEventingInterceptor interceptor={secondInterceptor}>
<ComponentWithTwoEvents
name="toBeInterceptedInFirstInterceptor"
eventOriginatorToBeIntercepted={evenOriginatorToBeInterceptedFirst}
/>
<ComponentWithTwoEvents
name="toBeInterceptedInSecondInterceptor"
eventOriginatorToBeIntercepted={evenOriginatorToBeInterceptedSecond}
/>
</NovaEventingInterceptor>
</NovaEventingInterceptor>
</NovaEventingProvider>
);

it("intercepts the event in second interceptor and does not bubble it up", async () => {
const { getByText } = render(<MultipleInterceptorsTestComponent />);
const button = getByText(
"toBeInterceptedInSecondInterceptor: Fire event to be intercepted",
);
button.click();
expect(callbackToBeCalledOnSecondIntercept).toHaveBeenCalled();
await waitFor(() => expect(mapEventMetadataMock).toHaveBeenCalled());
expect(bubbleMock).not.toHaveBeenCalled();
expect(callbackToBeCalledOnFirstIntercept).not.toHaveBeenCalled();
});

it("bubbles the event when interceptor returns the event", async () => {
const { getByText } = render(<MultipleInterceptorsTestComponent />);
const button = getByText(
"toBeInterceptedInFirstInterceptor: Fire event without intercept",
);
button.click();
expect(callbackToBeCalledOnSecondIntercept).not.toHaveBeenCalled();
expect(callbackToBeCalledOnFirstIntercept).not.toHaveBeenCalled();
await waitFor(() => expect(bubbleMock).toHaveBeenCalled());
carmenberndt marked this conversation as resolved.
Show resolved Hide resolved
});

it("intercepts the event in first interceptor and does not bubble it up", async () => {
const { getByText } = render(<MultipleInterceptorsTestComponent />);
const button = getByText(
"toBeInterceptedInFirstInterceptor: Fire event to be intercepted",
);
button.click();

await waitFor(() => expect(bubbleMock).not.toHaveBeenCalled());
expect(callbackToBeCalledOnSecondIntercept).not.toHaveBeenCalled();
await waitFor(() => expect(mapEventMetadataMock).toHaveBeenCalled());

expect(callbackToBeCalledOnFirstIntercept).toHaveBeenCalled();
});
});
70 changes: 59 additions & 11 deletions packages/nova-react/src/eventing/nova-eventing-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ export const NovaEventingProvider: React.FunctionComponent<
}

const reactEventing = React.useMemo(
generateEventing(eventingRef, mapperRef),
generateReactEventing(eventingRef, mapperRef),
[],
);

const reactUnmountEventing = React.useMemo(
generateEventing(unmountEventingRef, mapperRef),
generateReactEventing(unmountEventingRef, mapperRef),
[],
);

Expand Down Expand Up @@ -142,36 +142,62 @@ export const NovaEventingInterceptor: React.FunctionComponent<
interceptorRef.current = interceptor;
}

const { internal } = React.useContext(NovaEventingContext);
const { internal: rootInternal } = React.useContext(NovaEventingContext);

if (!internal) {
if (!rootInternal) {
invariant(
internal,
rootInternal,
"Nova Eventing provider must be initialized prior to creating NovaEventingInterceptor!",
);
}

const reactEventing = React.useMemo(
generateEventing(internal.eventingRef, internal.mapperRef, interceptorRef),
generateReactEventing(
rootInternal.eventingRef,
rootInternal.mapperRef,
interceptorRef,
),
[],
);

const reactUnmountEventing = React.useMemo(
generateEventing(
internal.unmountEventingRef,
internal.mapperRef,
generateReactEventing(
rootInternal.unmountEventingRef,
rootInternal.mapperRef,
interceptorRef,
),
[],
);

const eventing = React.useMemo(
generateEventing(rootInternal.eventingRef, interceptorRef),
[],
);

const unmountEventing = React.useMemo(
generateEventing(rootInternal.unmountEventingRef, interceptorRef),
[],
);

const eventingRef = React.useRef(eventing);
const unmountEventingRef = React.useRef(unmountEventing);

const internal: InternalEventingContext = React.useMemo(
() => ({
...rootInternal,
eventingRef,
unmountEventingRef,
}),
[eventingRef, unmountEventingRef],
);

const contextValue = React.useMemo(
() => ({
eventing: reactEventing,
unmountEventing: reactUnmountEventing,
internal,
}),
[reactEventing, reactUnmountEventing],
[reactEventing, reactUnmountEventing, internal],
);

return (
Expand All @@ -197,7 +223,7 @@ export const useNovaUnmountEventing = (): NovaReactEventing => {
return unmountEventing;
};

const generateEventing =
const generateReactEventing =
(
eventingRef: React.MutableRefObject<NovaEventing>,
mapperRef: React.MutableRefObject<
Expand Down Expand Up @@ -241,3 +267,25 @@ const generateEventing =
: Promise.resolve();
},
});

const generateEventing =
(
eventingRef: React.MutableRefObject<NovaEventing>,
interceptorRef?: React.MutableRefObject<
(event: EventWrapper) => Promise<EventWrapper | undefined>
>,
) =>
(): NovaEventing => ({
bubble: async (eventWrapper: EventWrapper) => {
carmenberndt marked this conversation as resolved.
Show resolved Hide resolved
if (!interceptorRef) {
return eventingRef.current.bubble(eventWrapper);
}

let eventToBubble: EventWrapper | undefined = eventWrapper;
eventToBubble = await interceptorRef.current(eventWrapper);

return eventToBubble
? eventingRef.current.bubble(eventToBubble)
: Promise.resolve();
},
});
Loading