diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx index d97fd6ce..461ef503 100644 --- a/client/src/App.test.tsx +++ b/client/src/App.test.tsx @@ -1,11 +1,11 @@ import App from "./App"; -import { store } from "./app/store"; +import { setupStore } from "./app/store"; import { render } from "@testing-library/react"; import { Provider } from "react-redux"; test("app renders", () => { render( - + ); diff --git a/client/src/app/store.ts b/client/src/app/store.ts index 46fcc228..bbb6eb83 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -3,36 +3,48 @@ import combatReducer from "../api/combatSlice"; import controlPointsReducer from "../api/controlPointsSlice"; import flightsReducer from "../api/flightsSlice"; import frontLinesReducer from "../api/frontLinesSlice"; +import iadsNetworkReducer from "../api/iadsNetworkSlice"; import mapReducer from "../api/mapSlice"; import navMeshReducer from "../api/navMeshSlice"; import supplyRoutesReducer from "../api/supplyRoutesSlice"; import tgosReducer from "../api/tgosSlice"; -import iadsNetworkReducer from "../api/iadsNetworkSlice"; import threatZonesReducer from "../api/threatZonesSlice"; -import unculledZonesReducer from "../api/unculledZonesSlice"; -import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; +import unculledZonesReducer from "../api/unculledZonesSlice"; +import { + Action, + PreloadedState, + ThunkAction, + combineReducers, + configureStore, +} from "@reduxjs/toolkit"; -export const store = configureStore({ - reducer: { - combat: combatReducer, - controlPoints: controlPointsReducer, - flights: flightsReducer, - frontLines: frontLinesReducer, - map: mapReducer, - navmeshes: navMeshReducer, - supplyRoutes: supplyRoutesReducer, - iadsNetwork: iadsNetworkReducer, - tgos: tgosReducer, - threatZones: threatZonesReducer, - [baseApi.reducerPath]: baseApi.reducer, - unculledZones: unculledZonesReducer, - }, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(baseApi.middleware), +const rootReducer = combineReducers({ + combat: combatReducer, + controlPoints: controlPointsReducer, + flights: flightsReducer, + frontLines: frontLinesReducer, + map: mapReducer, + navmeshes: navMeshReducer, + supplyRoutes: supplyRoutesReducer, + iadsNetwork: iadsNetworkReducer, + tgos: tgosReducer, + threatZones: threatZonesReducer, + [baseApi.reducerPath]: baseApi.reducer, + unculledZones: unculledZonesReducer, }); -export type AppDispatch = typeof store.dispatch; -export type RootState = ReturnType; +export function setupStore(preloadedState?: PreloadedState) { + return configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(baseApi.middleware), + preloadedState: preloadedState, + }); +} + +export type AppStore = ReturnType; +export type AppDispatch = AppStore["dispatch"]; +export type RootState = ReturnType; export type AppThunk = ThunkAction< ReturnType, RootState, diff --git a/client/src/components/aircraftlayer/AircraftLayer.test.tsx b/client/src/components/aircraftlayer/AircraftLayer.test.tsx new file mode 100644 index 00000000..3017c5cc --- /dev/null +++ b/client/src/components/aircraftlayer/AircraftLayer.test.tsx @@ -0,0 +1,53 @@ +import { renderWithProviders } from "../../testutils"; +import AircraftLayer from "./AircraftLayer"; +import { PropsWithChildren } from "react"; + +const mockLayerGroup = jest.fn(); +const mockMarker = jest.fn(); +jest.mock("react-leaflet", () => ({ + LayerGroup: (props: PropsWithChildren) => { + mockLayerGroup(props); + return <>{props.children}; + }, + Marker: (props: any) => { + mockMarker(props); + }, +})); + +test("layer is empty by default", async () => { + renderWithProviders(); + expect(mockLayerGroup).toHaveBeenCalledTimes(1); + expect(mockMarker).not.toHaveBeenCalled(); +}); + +test("layer has aircraft if non-empty", async () => { + renderWithProviders(, { + preloadedState: { + flights: { + flights: { + foo: { + id: "foo", + blue: true, + sidc: "", + position: { + lat: 0, + lng: 0, + }, + }, + bar: { + id: "bar", + blue: false, + sidc: "", + position: { + lat: 0, + lng: 0, + }, + }, + }, + selected: null, + }, + }, + }); + expect(mockLayerGroup).toHaveBeenCalledTimes(1); + expect(mockMarker).toHaveBeenCalledTimes(2); +}); diff --git a/client/src/index.tsx b/client/src/index.tsx index f6d829dd..2ea260f7 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,5 +1,5 @@ import App from "./App"; -import { store } from "./app/store"; +import { setupStore } from "./app/store"; import { SocketProvider } from "./components/socketprovider/socketprovider"; import "./index.css"; import * as serviceWorker from "./serviceWorker"; @@ -12,7 +12,7 @@ const root = ReactDOM.createRoot( ); root.render( - + diff --git a/client/src/testutils/index.tsx b/client/src/testutils/index.tsx new file mode 100644 index 00000000..64196f3e --- /dev/null +++ b/client/src/testutils/index.tsx @@ -0,0 +1,30 @@ +// https://redux.js.org/usage/writing-tests +import { setupStore } from "../app/store"; +import type { AppStore, RootState } from "../app/store"; +import type { PreloadedState } from "@reduxjs/toolkit"; +import { render } from "@testing-library/react"; +import type { RenderOptions } from "@testing-library/react"; +import React, { PropsWithChildren } from "react"; +import { Provider } from "react-redux"; + +// This type interface extends the default options for render from RTL, as well +// as allows the user to specify other things such as initialState, store. +interface ExtendedRenderOptions extends Omit { + preloadedState?: PreloadedState; + store?: AppStore; +} + +export function renderWithProviders( + ui: React.ReactElement, + { + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = setupStore(preloadedState), + ...renderOptions + }: ExtendedRenderOptions = {} +) { + function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element { + return {children}; + } + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; +}