Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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 === undefined) {
Comment thread
myty marked this conversation as resolved.
Outdated
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
199 changes: 199 additions & 0 deletions src/providers/network-connection-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
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";

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

type TypeFromKey<Type, Key extends keyof Type> = Type[Key];
Comment thread
myty marked this conversation as resolved.
Outdated

// #endregion Types

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

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

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

// #endregion Interfaces

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

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

// #endregion Mocks

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

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

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 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 connection: NetworkConnection = {
isOnline: true,
};
const { networkConnectionResults, TestComponent } = setupSut({
mockConnections: [connection],
});

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

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

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

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

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

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

const mockConnections: Array<NetworkConnection> = [
{
isOnline: true,
},
{
isOnline: true,
addEventListener: (
event: "change",
callback: VoidFunction
) => {
loadNetworkInformationCallback = callback;
},
},
{
isOnline: false,
},
];

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

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

act(() => loadNetworkInformationCallback());

// Assert
expect(networkConnectionResults.all.length).toEqual(
mockConnections.length
);
expect(networkConnectionResults.current).toEqual(
mockConnections[2]
);
});
});

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

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

await cleanup();

// Assert
expect(removeEventListener).toBeCalled();
});
});
});
83 changes: 83 additions & 0 deletions src/providers/network-connection-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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(() => {
const networkConnection = NetworkInformationUtils.getNetworkConnection();

if (networkConnection == null) {
return;
}

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

useEffect(function handleOnlineOfflineEvents() {
const createNetworkChangeHandler = (isOnline: boolean) => () => {
setState((prev) => ({
...prev,
isOnline,
}));
};

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

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

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

useEffect(
function handleNetworkChangeEvents() {
const networkConnection = NetworkInformationUtils.getNetworkConnection();
networkConnection?.addEventListener?.(
"change",
loadNetworkInformation
);

loadNetworkInformation();

return function cleanup() {
networkConnection?.removeEventListener?.(
"change",
loadNetworkInformation
);
};
},
[loadNetworkInformation]
);

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