diff --git a/src/__tests__/useChannel.tsx b/src/__tests__/useChannel.tsx index a9bb994..d8b89e3 100644 --- a/src/__tests__/useChannel.tsx +++ b/src/__tests__/useChannel.tsx @@ -2,33 +2,95 @@ import React from "react"; import { renderHook } from "@testing-library/react-hooks"; import { PusherProvider } from "../PusherProvider"; import { useChannel } from "../useChannel"; +import { cleanup } from "@testing-library/react"; + +beforeEach(() => { + cleanup(); + jest.resetAllMocks(); +}); jest.mock("pusher-js", () => { - const { PusherMock } = require("../mocks.ts"); - return { - __esModule: true, - default: jest.fn(() => new PusherMock()) - }; + const { PusherMock } = require("pusher-js-mock"); + // monkey patch missing function + PusherMock.prototype.disconnect = () => {}; + return PusherMock; +}); + +const config = { + clientKey: "client-key", + cluster: "ap4", + children: "Test" +}; + +test("should fill default options", () => { + const wrapper = ({ children }: any) => ( + {children} + ); + const { result, unmount } = renderHook(() => useChannel("my-channel"), { + wrapper + }); + + const { channel } = result.current; + expect(Object.keys(channel.callbacks)).toHaveLength(0); + + unmount(); }); -test("should subscribe to channel and listen to events", async () => { +test("should subscribe to channel and emit events", async () => { const onEvent = jest.fn(); const wrapper = ({ children }: any) => ( - - {children} - + {children} ); - const { result, unmount } = renderHook( - () => useChannel("my-channel", "my-event", onEvent), + const { result, unmount, rerender } = renderHook( + () => useChannel("my-channel", "my-event", onEvent, [], { skip: false }), { wrapper } ); - expect(result.current!.bind).toHaveBeenCalledTimes(1); + rerender(); + + const { channel } = result.current; - result.current!.emit("my-event", "test"); + channel.emit("my-event", "test"); + expect(channel.callbacks["my-event"]).toBeTruthy(); expect(onEvent).toHaveBeenCalledTimes(1); expect(onEvent).toHaveBeenCalledWith("test"); unmount(); - expect(result.current!.unbind).toHaveBeenCalledTimes(1); +}); + +test("should subscribe to channel as prop changes", () => { + const onEvent = jest.fn(); + const wrapper = ({ children }: any) => ( + {children} + ); + const { result, unmount, rerender } = renderHook( + ([a = "my-channel", b = "my-event", c = onEvent]: any) => + useChannel(a, b, c), + { wrapper } + ); + + rerender(["your-channel", "some-event", onEvent]); + const { channel } = result.current; + + // simulate event + channel.emit("some-event", "test"); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith("test"); + unmount(); +}); + +test("should skip channel subscription if option is passed", () => { + const wrapper = ({ children }: any) => ( + {children} + ); + const { result, unmount } = renderHook( + () => useChannel("a", "b", () => {}, [], { skip: true }), + { wrapper } + ); + + const { channel } = result.current; + expect(channel).toBeUndefined(); + + unmount(); }); diff --git a/src/useChannel.ts b/src/useChannel.ts index d566e17..6b4a1ce 100644 --- a/src/useChannel.ts +++ b/src/useChannel.ts @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; -import { Channel, EventCallback } from "pusher-js"; +import { EventCallback } from "pusher-js"; import invariant from "invariant"; import { usePusher } from "./usePusher"; @@ -26,8 +26,6 @@ export function useChannel( ) { // errors for missing arguments invariant(channelName, "channelName required to subscribe to a channel"); - invariant(eventName, "eventName required to bind to an event"); - invariant(onEvent, "onEvent required to callback on event"); // initialise defaults const defaultOptions = { skip: false }; @@ -39,35 +37,50 @@ export function useChannel( // hook setup const { client } = usePusher(); - const callback = useCallback(eventHandler, deps); - const [channel, setChannel] = useState(); + const [channel, setChannel] = useState(); + /** + * Channel subscription + */ useEffect(() => { - if (client && !hookOptions.skip && channelName) { + if (client.current && !hookOptions.skip) { // subscribe to the channel - const pusherChannel = client.subscribe(channelName); - // if there's an eventName, bind to it. - eventName && pusherChannel.bind(eventName, callback); - - // store the ref for cleanup + const pusherChannel = client.current.subscribe(channelName); setChannel(pusherChannel); } - }, [client, callback, hookOptions.skip, eventName, channelName]); + }, [client.current, hookOptions.skip]); - // cleanup on unmount - useEffect( - () => () => { - if (client && channel) { - client.unsubscribe(channelName); - } + const previousChannelName = useRef(); + useEffect(() => { + if (previousChannelName.current === channelName) return; + const clientRef = client.current; + return () => { + clientRef.unsubscribe(channelName); + }; + }); + // track channelName for unsubscription. + useEffect(() => { + previousChannelName.current = channelName; + }); + /** + * Event binding + */ + const callback = useCallback(eventHandler, deps); + useEffect(() => { + if (channel && eventName && !hookOptions.skip) { + channel.bind(eventName, callback); + } + }, [channel, eventName, hookOptions.skip, callback]); + + // when the callback changes, unbind the old one. + useEffect(() => { + return () => { if (client && channel && eventName) { channel.unbind(eventName, callback); } - }, - [client, channel] - ); + }; + }, [client, channel, callback, eventName]); - // return channel instance back for unsafe things like channel.trigger() - return channel; + return { channel }; }