Skip to content

Network connection provider and hook #80

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 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
6 changes: 6 additions & 0 deletions src/contexts/network-connection-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NetworkConnection } from "andculturecode-javascript-core";
import React from "react";

export const NetworkConnectionContext = React.createContext<
NetworkConnection | undefined
>(undefined);
59 changes: 59 additions & 0 deletions src/hooks/use-network-connection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { render } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";
import {
NetworkConnection,
NetworkInformationUtils,
} from "andculturecode-javascript-core";
import React from "react";
import { NetworkConnectionProvider } from "../providers/network-connection-provider";
import { useNetworkConnection } from "./use-network-connection";

// -----------------------------------------------------------------------------------------
// #region Mocks
// -----------------------------------------------------------------------------------------

const getNetworkConnectionMock = jest.spyOn(
NetworkInformationUtils,
"getNetworkConnection"
);

// #endregion Mocks

describe("useNetworkConnection", () => {
describe("when used outside NetworkConnectionProvider", () => {
it("throws error", () => {
// Arrange & Act
const { result } = renderHook(() => useNetworkConnection());

// Assert
expect(result.error).toBeDefined();
});
});

describe("when used inside NetworkConnectionProvider", () => {
it("returns network connection information", () => {
// Arrange
let networkConnection: NetworkConnection;
const expectedNetworkConnection: NetworkConnection = {
isOnline: true,
};

getNetworkConnectionMock.mockReturnValue(expectedNetworkConnection);

const TestComponent = () => {
networkConnection = useNetworkConnection();
return <div></div>;
};

// Act
render(
<NetworkConnectionProvider>
<TestComponent />
</NetworkConnectionProvider>
);

// Assert
expect(networkConnection).toEqual(expectedNetworkConnection);
});
});
});
18 changes: 18 additions & 0 deletions src/hooks/use-network-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext } from "react";
import { NetworkConnection } from "andculturecode-javascript-core";
import { NetworkConnectionContext } from "../contexts/network-connection-context";

/**
* Hook that returns the current network connection information
*/
export const useNetworkConnection = (): NetworkConnection => {
const networkConnection = useContext(NetworkConnectionContext);

if (networkConnection == null) {
throw new Error(
"useNetworkConnection must be used within a NetworkConnectionProvider component"
);
}

return networkConnection;
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
NestedRoutesByProperty,
NestedRoutesByPropertyProps,
} from "./components/routing/nested-routes-by-property";
export { NetworkConnectionProvider } from "./providers/network-connection-provider";
export { Redirects, RedirectsProps } from "./components/routing/redirects";

// #endregion Components
Expand All @@ -31,6 +32,7 @@ export { useAsyncEffect } from "./hooks/use-async-effect";
export { useCancellablePromise } from "./hooks/use-cancellable-promise";
export { useDebounce } from "./hooks/use-debounce";
export { useLocalization } from "./hooks/use-localization";
export { useNetworkConnection } from "./hooks/use-network-connection";
export { useOnClickOutside } from "./hooks/use-onclick-outside";
export { usePageErrors } from "./hooks/use-page-errors";
export { useSortedAlphabetically } from "./hooks/use-sorted-alphabetically";
Expand Down
216 changes: 216 additions & 0 deletions src/providers/network-connection-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React from "react";
import { act, cleanup, render } from "@testing-library/react";
import { NetworkConnectionProvider } from "./network-connection-provider";
import {
NetworkConnection,
NetworkInformationUtils,
} from "andculturecode-javascript-core";
import { useNetworkConnection } from "../hooks/use-network-connection";
import { FactoryType } from "../tests/factories/factory-type";
import { Factory } from "rosie";

// -----------------------------------------------------------------------------------------
// #region Types
// -----------------------------------------------------------------------------------------

type TypeFromKey<Type, Key extends keyof Type> = Type[Key];

// #endregion Types

// -----------------------------------------------------------------------------------------
// #region Interfaces
// -----------------------------------------------------------------------------------------

interface SetupSutOptions {
mockConnections?: Array<Partial<NetworkConnection>>;
}

interface SetupSutResults {
TestComponent: () => JSX.Element;
networkConnectionResults: {
all: Array<NetworkConnection>;
current?: NetworkConnection;
};
}

// #endregion Interfaces

// -----------------------------------------------------------------------------------------
// #region Setup
// -----------------------------------------------------------------------------------------

const getNetworkConnectionMock = jest.spyOn(
NetworkInformationUtils,
"getNetworkConnection"
);

const setupMocks = (mockConnections: Array<Partial<NetworkConnection>>) => {
getNetworkConnectionMock.mockReset();
for (let index = 0; index < mockConnections.length; index++) {
const mockImplementation =
index === mockConnections.length - 1
? getNetworkConnectionMock.mockImplementation
: getNetworkConnectionMock.mockImplementationOnce;

mockImplementation(() => {
return {
isOnline: true,
...mockConnections[index],
};
});
}
};

const setupSut = (options?: SetupSutOptions): SetupSutResults => {
const { mockConnections = [] } = options ?? {};

setupMocks(mockConnections);

const networkConnectionResults: TypeFromKey<
SetupSutResults,
"networkConnectionResults"
> = {
all: [] as NetworkConnection[],
};

function TestComponent() {
const connection = useNetworkConnection();
networkConnectionResults.all.push(connection);
networkConnectionResults.current = connection;

return <div></div>;
}

return {
TestComponent,
networkConnectionResults,
};
};

// #endregion Setup

describe("NetworkConnectionProvider", () => {
it("renders initial network connection state", () => {
// Arrange
const expectedConnection = Factory.build<NetworkConnection>(
FactoryType.NetworkConnection
);
const { networkConnectionResults, TestComponent } = setupSut({
mockConnections: [
Factory.build<NetworkConnection>(FactoryType.NetworkConnection),
expectedConnection,
],
});

// Act
render(
<NetworkConnectionProvider>
<TestComponent />
</NetworkConnectionProvider>
);

// Assert
expect(networkConnectionResults.all.length).toEqual(2);
expect(networkConnectionResults.current).toEqual(expectedConnection);
});

it("adds an event listener", () => {
// Arrange
const addEventListener = jest.fn();
const networkConnection = Factory.build<NetworkConnection>(
FactoryType.NetworkConnection,
{
addEventListener,
}
);
const { TestComponent } = setupSut({
mockConnections: [networkConnection],
});

// Act
render(
<NetworkConnectionProvider>
<TestComponent />
</NetworkConnectionProvider>
);

// Assert
expect(addEventListener).toBeCalled();
});

describe("when change event is called", () => {
it("loads network connection into state", () => {
// Arrange
let changeEventCallback = () => {};

const expectedNetworkConnection = Factory.build<NetworkConnection>(
FactoryType.NetworkConnection
);

const mockConnections: Array<NetworkConnection> = [
Factory.build<NetworkConnection>(FactoryType.NetworkConnection),
Factory.build<NetworkConnection>(
FactoryType.NetworkConnection,
{
addEventListener: (
event: "change",
callback: VoidFunction
) => {
changeEventCallback = callback;
},
}
),
expectedNetworkConnection,
];

const { networkConnectionResults, TestComponent } = setupSut({
mockConnections,
});

// Act
render(
<NetworkConnectionProvider>
<TestComponent />
</NetworkConnectionProvider>
);

act(() => changeEventCallback());

// Assert
expect(networkConnectionResults.all.length).toEqual(
mockConnections.length
);
expect(networkConnectionResults.current).toEqual(
expectedNetworkConnection
);
});
});

describe("when unmounted", () => {
it("calls removeEventlistener for cleanup", async () => {
// Arrange
const removeEventListener = jest.fn();
const networkConnection = Factory.build<NetworkConnection>(
FactoryType.NetworkConnection,
{
removeEventListener,
}
);
const { TestComponent } = setupSut({
mockConnections: [networkConnection],
});

// Act
render(
<NetworkConnectionProvider>
<TestComponent />
</NetworkConnectionProvider>
);

await cleanup();

// Assert
expect(removeEventListener).toBeCalled();
});
});
});
76 changes: 76 additions & 0 deletions src/providers/network-connection-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, {
PropsWithChildren,
useCallback,
useEffect,
useState,
} from "react";
import {
NetworkConnection,
NetworkInformationUtils,
} from "andculturecode-javascript-core";
import { NetworkConnectionContext } from "../contexts/network-connection-context";

/**
* Wrapper provider component that provides context to the `useNetworkConnection` hook
*/
export const NetworkConnectionProvider: React.FC = (
props: PropsWithChildren<unknown>
) => {
const { children } = props;

const [state, setState] = useState<NetworkConnection | undefined>(
NetworkInformationUtils.getNetworkConnection()
);

const loadNetworkInformation = useCallback((isOnline?: boolean) => {
const networkConnection = NetworkInformationUtils.getNetworkConnection();

if (networkConnection == null) {
return;
}

setState((prev) => ({
...prev,
...networkConnection,
isOnline: isOnline ?? networkConnection.isOnline,
}));
}, []);

useEffect(
function handleNetworkChangeEvents() {
const networkConnection = NetworkInformationUtils.getNetworkConnection();

const createNetworkChangeHandler = (isOnline?: boolean) => () =>
loadNetworkInformation(isOnline);

const handleNetworkChange = createNetworkChangeHandler();
const handleOffline = createNetworkChangeHandler(false);
const handleOnline = createNetworkChangeHandler(true);

networkConnection?.addEventListener?.(
"change",
handleNetworkChange
);
window?.addEventListener?.("online", handleOnline);
window?.addEventListener?.("offline", handleOffline);

loadNetworkInformation();

return function cleanup() {
networkConnection?.removeEventListener?.(
"change",
handleNetworkChange
);
window?.removeEventListener?.("online", handleOnline);
window?.removeEventListener?.("offline", handleOffline);
};
},
[loadNetworkInformation]
);

return (
<NetworkConnectionContext.Provider value={state}>
{children}
</NetworkConnectionContext.Provider>
);
};
Loading