From a88f613da7e5567dbfdebd7df94f94507c47c6b5 Mon Sep 17 00:00:00 2001
From: Michael Hunteman <michael@huntm.net>
Date: Fri, 27 Sep 2024 08:43:02 -0700
Subject: Add vitests

---
 client/package.json                           | 10 ++-
 client/src/AppThemeProvider.tsx               | 94 +++++++++++++++++++++++++++
 client/src/ThemeContextProvider.tsx           | 94 ---------------------------
 client/src/components/AdminLogin.test.tsx     | 30 +++++++++
 client/src/components/Dashboard.tsx           |  1 +
 client/src/components/Desktop.tsx             |  2 +-
 client/src/components/GlobalSnackbar.test.tsx | 28 ++++++++
 client/src/components/GuestLogin.test.tsx     | 29 +++++++++
 client/src/components/Mobile.tsx              |  2 +-
 client/src/components/RsvpForm.test.tsx       | 31 +++++++++
 client/src/main.tsx                           | 67 ++-----------------
 client/src/mocks/browser.ts                   |  4 --
 client/src/mocks/handlers.ts                  | 57 ++++++++++------
 client/src/mocks/server.ts                    |  4 ++
 client/src/mocks/worker.ts                    |  4 ++
 client/src/models.ts                          |  2 +-
 client/src/renderWithProviders.tsx            | 28 ++++++++
 client/src/routes.tsx                         | 59 +++++++++++++++++
 client/src/setup.ts                           |  6 ++
 client/src/slices/snackbarSlice.ts            |  3 +-
 client/src/store.ts                           | 37 +++++++----
 client/tsconfig.json                          |  2 +-
 client/vite.config.ts                         | 12 +++-
 server/post.json                              | 10 ---
 24 files changed, 403 insertions(+), 213 deletions(-)
 create mode 100644 client/src/AppThemeProvider.tsx
 delete mode 100644 client/src/ThemeContextProvider.tsx
 create mode 100644 client/src/components/AdminLogin.test.tsx
 create mode 100644 client/src/components/GlobalSnackbar.test.tsx
 create mode 100644 client/src/components/GuestLogin.test.tsx
 create mode 100644 client/src/components/RsvpForm.test.tsx
 delete mode 100644 client/src/mocks/browser.ts
 create mode 100644 client/src/mocks/server.ts
 create mode 100644 client/src/mocks/worker.ts
 create mode 100644 client/src/renderWithProviders.tsx
 create mode 100644 client/src/routes.tsx
 create mode 100644 client/src/setup.ts
 delete mode 100644 server/post.json

diff --git a/client/package.json b/client/package.json
index 0d6bab2..0990aa3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -7,7 +7,8 @@
     "dev": "bunx --bun vite --host",
     "build": "tsc && vite build",
     "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
-    "preview": "vite preview"
+    "preview": "vite preview",
+    "test": "vitest"
   },
   "dependencies": {
     "@emotion/react": "^11.13.3",
@@ -28,6 +29,9 @@
   },
   "devDependencies": {
     "@hookform/devtools": "^4.3.1",
+    "@testing-library/jest-dom": "^6.5.0",
+    "@testing-library/react": "^16.0.1",
+    "@testing-library/user-event": "^14.5.2",
     "@types/bun": "^1.1.0",
     "@types/react": "^18.2.55",
     "@types/react-dom": "^18.2.19",
@@ -37,8 +41,10 @@
     "eslint": "^8.56.0",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-refresh": "^0.4.5",
+    "jsdom": "^25.0.0",
     "msw": "^2.2.1",
     "typescript": "^5.2.2",
-    "vite": "^5.4.2"
+    "vite": "^5.4.2",
+    "vitest": "^2.1.1"
   }
 }
diff --git a/client/src/AppThemeProvider.tsx b/client/src/AppThemeProvider.tsx
new file mode 100644
index 0000000..a88c328
--- /dev/null
+++ b/client/src/AppThemeProvider.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { ReactNode, createContext, useMemo, useState } from 'react';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import useMediaQuery from '@mui/material/useMediaQuery';
+import type { PaletteMode } from '@mui/material';
+
+type ThemeContextType = {
+  toggleColorMode: () => void;
+};
+
+type ThemeProviderProps = {
+  children: ReactNode;
+};
+
+export const ThemeContext = createContext<ThemeContextType>({
+  toggleColorMode: () => {},
+});
+
+function AppThemeProvider({ children }: ThemeProviderProps) {
+  const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+  const [mode, setMode] = useState<'light' | 'dark'>(
+    prefersDarkMode ? 'dark' : 'light'
+  );
+
+  const toggleColorMode = () => {
+    setMode((prevMode: PaletteMode) =>
+      prevMode === 'light' ? 'dark' : 'light'
+    );
+  };
+
+  const getDesignTokens = (mode: PaletteMode) => ({
+    palette: {
+      mode,
+      ...(mode === 'light'
+        ? {
+            primary: {
+              main: '#FFAB91',
+            },
+          }
+        : {
+            primary: {
+              main: '#FF8A65',
+            },
+          }),
+    },
+  });
+
+  const colorTheme = createTheme(getDesignTokens(mode));
+
+  const roboto = {
+    sx: {
+      fontFamily: 'Roboto, sans-serif',
+    },
+  };
+
+  const theme = useMemo(
+    () =>
+      createTheme({
+        ...colorTheme,
+        typography: {
+          fontFamily: ['Playwrite US Trad', 'cursive'].join(','),
+          button: {
+            textTransform: 'none',
+          },
+          body1: {
+            lineHeight: 1.8,
+          },
+        },
+        components: {
+          MuiInputBase: {
+            defaultProps: roboto,
+          },
+          MuiInputLabel: {
+            defaultProps: roboto,
+          },
+          MuiAlert: {
+            defaultProps: roboto,
+          },
+          MuiFormHelperText: {
+            defaultProps: roboto,
+          },
+        },
+      }),
+    [mode]
+  );
+
+  return (
+    <ThemeContext.Provider value={{ toggleColorMode }}>
+      <ThemeProvider theme={theme}>{children}</ThemeProvider>
+    </ThemeContext.Provider>
+  );
+}
+
+export default AppThemeProvider;
diff --git a/client/src/ThemeContextProvider.tsx b/client/src/ThemeContextProvider.tsx
deleted file mode 100644
index 5e1e2e8..0000000
--- a/client/src/ThemeContextProvider.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from 'react';
-import { ReactNode, createContext, useMemo, useState } from 'react';
-import { ThemeProvider, createTheme } from '@mui/material/styles';
-import useMediaQuery from '@mui/material/useMediaQuery';
-import type { PaletteMode } from '@mui/material';
-
-type ThemeContextType = {
-  toggleColorMode: () => void;
-};
-
-type ThemeProviderProps = {
-  children: ReactNode;
-};
-
-export const ThemeContext = createContext<ThemeContextType>({
-  toggleColorMode: () => {},
-});
-
-function ThemeContextProvider({ children }: ThemeProviderProps) {
-  const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
-  const [mode, setMode] = useState<'light' | 'dark'>(
-    prefersDarkMode ? 'dark' : 'light'
-  );
-
-  const toggleColorMode = () => {
-    setMode((prevMode: PaletteMode) =>
-      prevMode === 'light' ? 'dark' : 'light'
-    );
-  };
-
-  const getDesignTokens = (mode: PaletteMode) => ({
-    palette: {
-      mode,
-      ...(mode === 'light'
-        ? {
-            primary: {
-              main: '#FFAB91',
-            },
-          }
-        : {
-            primary: {
-              main: '#FF8A65',
-            },
-          }),
-    },
-  });
-
-  const colorTheme = createTheme(getDesignTokens(mode));
-
-  const roboto = {
-    sx: {
-      fontFamily: 'Roboto, sans-serif',
-    },
-  };
-
-  const theme = useMemo(
-    () =>
-      createTheme({
-        ...colorTheme,
-        typography: {
-          fontFamily: ['Playwrite US Trad', 'cursive'].join(','),
-          button: {
-            textTransform: 'none',
-          },
-          body1: {
-            lineHeight: 1.8,
-          },
-        },
-        components: {
-          MuiInputBase: {
-            defaultProps: roboto,
-          },
-          MuiInputLabel: {
-            defaultProps: roboto,
-          },
-          MuiAlert: {
-            defaultProps: roboto,
-          },
-          MuiFormHelperText: {
-            defaultProps: roboto,
-          },
-        },
-      }),
-    [mode]
-  );
-
-  return (
-    <ThemeContext.Provider value={{ toggleColorMode }}>
-      <ThemeProvider theme={theme}>{children}</ThemeProvider>
-    </ThemeContext.Provider>
-  );
-}
-
-export default ThemeContextProvider;
diff --git a/client/src/components/AdminLogin.test.tsx b/client/src/components/AdminLogin.test.tsx
new file mode 100644
index 0000000..feffadf
--- /dev/null
+++ b/client/src/components/AdminLogin.test.tsx
@@ -0,0 +1,30 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import { fireEvent, screen } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { createMemoryRouter, RouterProvider } from 'react-router-dom';
+import { renderWithProviders } from '../renderWithProviders';
+import routes from '../routes';
+
+describe('Admin Login', async () => {
+  const memoryRouter = createMemoryRouter(routes, {
+    initialEntries: ['/admin/login'],
+  });
+  it('can log in', async () => {
+    const { getByLabelText, getByRole, findByText } = renderWithProviders(
+      <RouterProvider router={memoryRouter} />
+    );
+    const user = userEvent.setup();
+
+    await user.type(getByLabelText(/username/i), 'username');
+    await user.type(getByLabelText(/password/i), 'password');
+    fireEvent.click(getByRole('button', { name: 'Log in' }));
+    expect(await findByText(/first name/i)).toBeInTheDocument();
+    expect(await findByText(/last name/i)).toBeInTheDocument();
+    expect(await findByText(/attendance/i)).toBeInTheDocument();
+    expect(await findByText(/email/i)).toBeInTheDocument();
+    expect(await findByText(/message/i)).toBeInTheDocument();
+    expect(await findByText(/party size/i)).toBeInTheDocument();
+  });
+});
diff --git a/client/src/components/Dashboard.tsx b/client/src/components/Dashboard.tsx
index 985e48a..56b50a2 100644
--- a/client/src/components/Dashboard.tsx
+++ b/client/src/components/Dashboard.tsx
@@ -48,6 +48,7 @@ function Dashboard() {
     columns,
     data: guests,
     muiPaginationProps: {
+      color: 'primary',
       shape: 'rounded',
       showRowsPerPage: false,
       variant: 'outlined',
diff --git a/client/src/components/Desktop.tsx b/client/src/components/Desktop.tsx
index 0aa4357..d7447f3 100644
--- a/client/src/components/Desktop.tsx
+++ b/client/src/components/Desktop.tsx
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
 import { Button, IconButton, useTheme } from '@mui/material';
 import DarkModeIcon from '@mui/icons-material/DarkMode';
 import LightModeIcon from '@mui/icons-material/LightMode';
-import { ThemeContext } from '../ThemeContextProvider';
+import { ThemeContext } from '../AppThemeProvider';
 import pages from '../pages';
 
 function Desktop() {
diff --git a/client/src/components/GlobalSnackbar.test.tsx b/client/src/components/GlobalSnackbar.test.tsx
new file mode 100644
index 0000000..2643816
--- /dev/null
+++ b/client/src/components/GlobalSnackbar.test.tsx
@@ -0,0 +1,28 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { createMemoryRouter, RouterProvider } from 'react-router-dom';
+import { renderWithProviders } from '../renderWithProviders';
+import routes from '../routes';
+import { showSnackbar } from '../slices/snackbarSlice';
+import setupStore from '../store';
+
+describe('Global Snackbar', async () => {
+  const memoryRouter = createMemoryRouter(routes, {
+    initialEntries: ['/'],
+  });
+  it('displays message', async () => {
+    const store = setupStore();
+    store.dispatch(
+      showSnackbar({
+        message: 'message',
+        severity: 'success',
+      })
+    );
+    const { findByText } = renderWithProviders(
+      <RouterProvider router={memoryRouter} />,
+      { store }
+    );
+    expect(await findByText(/message/i)).toBeInTheDocument();
+  });
+});
diff --git a/client/src/components/GuestLogin.test.tsx b/client/src/components/GuestLogin.test.tsx
new file mode 100644
index 0000000..b31a00a
--- /dev/null
+++ b/client/src/components/GuestLogin.test.tsx
@@ -0,0 +1,29 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { createMemoryRouter, RouterProvider } from 'react-router-dom';
+import { renderWithProviders } from '../renderWithProviders';
+import routes from '../routes';
+
+describe('Guest Login', async () => {
+  const memoryRouter = createMemoryRouter(routes, {
+    initialEntries: ['/guests/login'],
+  });
+  it('can log in', async () => {
+    const { getByLabelText, getByRole, findByLabelText } = renderWithProviders(
+      <RouterProvider router={memoryRouter} />
+    );
+    const user = userEvent.setup();
+
+    await user.type(getByLabelText(/first name/i), 'Michael');
+    await user.type(getByLabelText(/last name/i), 'Hunteman');
+    fireEvent.click(getByRole('button', { name: 'Log in' }));
+    expect(await findByLabelText(/accept/i)).not.toBeChecked();
+    expect(await findByLabelText(/decline/i)).toBeChecked();
+    expect(await findByLabelText(/email/i)).toHaveValue('');
+    expect(await findByLabelText(/party size/i)).toHaveValue(1);
+    expect(await findByLabelText(/message to the couple/i)).toHaveValue('');
+  });
+});
diff --git a/client/src/components/Mobile.tsx b/client/src/components/Mobile.tsx
index 38aa20b..d8510c7 100644
--- a/client/src/components/Mobile.tsx
+++ b/client/src/components/Mobile.tsx
@@ -5,7 +5,7 @@ import { Button, IconButton, Menu, MenuItem } from '@mui/material';
 import DarkModeIcon from '@mui/icons-material/DarkMode';
 import LightModeIcon from '@mui/icons-material/LightMode';
 import { useTheme } from '@mui/material/styles';
-import { ThemeContext } from '../ThemeContextProvider';
+import { ThemeContext } from '../AppThemeProvider';
 import MenuIcon from '@mui/icons-material/Menu';
 import pages from '../pages';
 
diff --git a/client/src/components/RsvpForm.test.tsx b/client/src/components/RsvpForm.test.tsx
new file mode 100644
index 0000000..a871268
--- /dev/null
+++ b/client/src/components/RsvpForm.test.tsx
@@ -0,0 +1,31 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { createMemoryRouter, RouterProvider } from 'react-router-dom';
+import { renderWithProviders } from '../renderWithProviders';
+import routes from '../routes';
+import setupStore from '../store';
+import { initialGuest } from '../mocks/handlers';
+import { setGuest } from '../slices/auth/guestSlice';
+
+describe('RSVP form', async () => {
+  const memoryRouter = createMemoryRouter(routes, {
+    initialEntries: ['/rsvp'],
+  });
+  it('can submit', async () => {
+    const store = setupStore();
+    store.dispatch(setGuest(initialGuest));
+    const { getByLabelText, getByRole, findByText } = renderWithProviders(
+      <RouterProvider router={memoryRouter} />,
+      { store }
+    );
+    const user = userEvent.setup();
+
+    fireEvent.click(getByLabelText(/accept/i), { target: { clicked: true } });
+    await user.type(getByLabelText(/email/i), 'mhunteman@cox.net');
+    fireEvent.click(getByRole('button', { name: 'RSVP' }));
+    expect(await findByText(/RSVP updated/i)).toBeInTheDocument();
+  });
+});
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 0e5a2f7..43e61c0 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -2,72 +2,19 @@ import React from 'react';
 import ReactDOM from 'react-dom/client';
 import { createBrowserRouter, RouterProvider } from 'react-router-dom';
 import { Provider } from 'react-redux';
-import App from './App';
-import store from './store';
-import ThemeContextProvider from './ThemeContextProvider';
-import Schedule from './components/Schedule';
-import Registry from './components/Registry';
-import GuestLogin from './components/GuestLogin';
-import Rsvp from './components/Rsvp';
-import RsvpForm from './components/RsvpForm';
-import Home from './components/Home';
-import AdminLogin from './components/AdminLogin';
-import Admin from './components/Admin';
-import Dashboard from './components/Dashboard';
+import setupStore from './store';
+import AppThemeProvider from './AppThemeProvider';
+import routes from './routes';
 import './main.css';
 
-const router = createBrowserRouter([
-  {
-    element: <App />,
-    children: [
-      {
-        path: '/',
-        element: <Home />,
-      },
-      {
-        path: 'schedule',
-        element: <Schedule />,
-      },
-      {
-        path: 'registry',
-        element: <Registry />,
-      },
-      {
-        path: 'guests/login',
-        element: <GuestLogin />,
-      },
-      {
-        path: 'admin/login',
-        element: <AdminLogin />,
-      },
-      {
-        element: <Rsvp />,
-        children: [
-          {
-            path: 'rsvp',
-            element: <RsvpForm />,
-          },
-        ],
-      },
-      {
-        element: <Admin />,
-        children: [
-          {
-            path: 'dashboard',
-            element: <Dashboard />,
-          },
-        ],
-      },
-    ],
-  },
-]);
+const router = createBrowserRouter(routes);
 
 ReactDOM.createRoot(document.getElementById('root')!).render(
   <React.StrictMode>
-    <Provider store={store}>
-      <ThemeContextProvider>
+    <Provider store={setupStore()}>
+      <AppThemeProvider>
         <RouterProvider router={router} />
-      </ThemeContextProvider>
+      </AppThemeProvider>
     </Provider>
   </React.StrictMode>
 );
diff --git a/client/src/mocks/browser.ts b/client/src/mocks/browser.ts
deleted file mode 100644
index 0a56427..0000000
--- a/client/src/mocks/browser.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { setupWorker } from 'msw/browser';
-import { handlers } from './handlers';
-
-export const worker = setupWorker(...handlers);
diff --git a/client/src/mocks/handlers.ts b/client/src/mocks/handlers.ts
index 0e882ee..153a70c 100644
--- a/client/src/mocks/handlers.ts
+++ b/client/src/mocks/handlers.ts
@@ -3,28 +3,43 @@ import { nanoid } from '@reduxjs/toolkit';
 
 const token = nanoid();
 
+export const initialGuest = {
+  guest: {
+    id: 1,
+    firstName: 'Michael',
+    lastName: 'Hunteman',
+    attendance: 'decline',
+    email: '',
+    message: '',
+    partySize: 1,
+  },
+  token: token,
+};
+
+export const updatedGuest = {
+  id: 1,
+  firstName: 'Michael',
+  lastName: 'Hunteman',
+  attendance: 'accept',
+  email: 'mhunteman@cox.net',
+  message: '',
+  partySize: 1,
+  partyList: [],
+};
+
+export const guests = {
+  guests: [initialGuest],
+  token: token,
+};
+
 export const handlers = [
-  http.post('/guests/login', () => {
-    return HttpResponse.json({
-      guest: {
-        id: 1,
-        firstName: 'Michael',
-        lastName: 'Hunteman',
-        attendance: 'false',
-        email: '',
-        message: '',
-      },
-      token,
-    });
+  http.post(`${import.meta.env.VITE_BASE_URL}guests/login`, () => {
+    return HttpResponse.json(initialGuest);
+  }),
+  http.put(`${import.meta.env.VITE_BASE_URL}guests/1`, () => {
+    return HttpResponse.json(updatedGuest);
   }),
-  http.patch('/guests/1', () => {
-    return HttpResponse.json({
-      id: 1,
-      firstName: 'Michael',
-      lastName: 'Hunteman',
-      attendance: 'true',
-      email: '',
-      message: '',
-    });
+  http.post(`${import.meta.env.VITE_BASE_URL}admin/login`, () => {
+    return HttpResponse.json(guests);
   }),
 ];
diff --git a/client/src/mocks/server.ts b/client/src/mocks/server.ts
new file mode 100644
index 0000000..e52fee0
--- /dev/null
+++ b/client/src/mocks/server.ts
@@ -0,0 +1,4 @@
+import { setupServer } from 'msw/node';
+import { handlers } from './handlers';
+
+export const server = setupServer(...handlers);
diff --git a/client/src/mocks/worker.ts b/client/src/mocks/worker.ts
new file mode 100644
index 0000000..0a56427
--- /dev/null
+++ b/client/src/mocks/worker.ts
@@ -0,0 +1,4 @@
+import { setupWorker } from 'msw/browser';
+import { handlers } from './handlers';
+
+export const worker = setupWorker(...handlers);
diff --git a/client/src/models.ts b/client/src/models.ts
index 6840a46..201a969 100644
--- a/client/src/models.ts
+++ b/client/src/models.ts
@@ -6,7 +6,7 @@ export interface Guest {
   email?: string;
   message?: string;
   partySize?: number;
-  partyList?: Array<Name>;
+  partyList?: Name[];
 }
 
 export interface Name {
diff --git a/client/src/renderWithProviders.tsx b/client/src/renderWithProviders.tsx
new file mode 100644
index 0000000..476fdb1
--- /dev/null
+++ b/client/src/renderWithProviders.tsx
@@ -0,0 +1,28 @@
+import React, { PropsWithChildren, ReactElement } from 'react';
+import { render } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import setupStore from './store';
+import type { AppStore, RootState } from './store';
+import type { RenderOptions } from '@testing-library/react';
+
+interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
+  preloadedState?: Partial<RootState>;
+  store?: AppStore;
+}
+
+export function renderWithProviders(
+  ui: ReactElement,
+  extendedRenderOptions: ExtendedRenderOptions = {}
+) {
+  const {
+    preloadedState = {},
+    store = setupStore(preloadedState),
+    ...renderOptions
+  } = extendedRenderOptions;
+
+  const Wrapper = ({ children }: PropsWithChildren) => (
+    <Provider store={store}>{children}</Provider>
+  );
+
+  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
+}
diff --git a/client/src/routes.tsx b/client/src/routes.tsx
new file mode 100644
index 0000000..3fec783
--- /dev/null
+++ b/client/src/routes.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import App from './App';
+import Schedule from './components/Schedule';
+import Registry from './components/Registry';
+import GuestLogin from './components/GuestLogin';
+import Rsvp from './components/Rsvp';
+import RsvpForm from './components/RsvpForm';
+import Home from './components/Home';
+import AdminLogin from './components/AdminLogin';
+import Admin from './components/Admin';
+import Dashboard from './components/Dashboard';
+
+const routes = [
+  {
+    element: <App />,
+    children: [
+      {
+        path: '/',
+        element: <Home />,
+      },
+      {
+        path: 'schedule',
+        element: <Schedule />,
+      },
+      {
+        path: 'registry',
+        element: <Registry />,
+      },
+      {
+        path: 'guests/login',
+        element: <GuestLogin />,
+      },
+      {
+        path: 'admin/login',
+        element: <AdminLogin />,
+      },
+      {
+        element: <Rsvp />,
+        children: [
+          {
+            path: 'rsvp',
+            element: <RsvpForm />,
+          },
+        ],
+      },
+      {
+        element: <Admin />,
+        children: [
+          {
+            path: 'dashboard',
+            element: <Dashboard />,
+          },
+        ],
+      },
+    ],
+  },
+];
+
+export default routes;
diff --git a/client/src/setup.ts b/client/src/setup.ts
new file mode 100644
index 0000000..0abb5f3
--- /dev/null
+++ b/client/src/setup.ts
@@ -0,0 +1,6 @@
+import { afterAll, afterEach, beforeAll } from 'vitest';
+import { server } from './mocks/server';
+
+beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
+afterAll(() => server.close());
+afterEach(() => server.resetHandlers());
diff --git a/client/src/slices/snackbarSlice.ts b/client/src/slices/snackbarSlice.ts
index f76b133..82532ec 100644
--- a/client/src/slices/snackbarSlice.ts
+++ b/client/src/slices/snackbarSlice.ts
@@ -1,10 +1,11 @@
 import { createSlice } from '@reduxjs/toolkit';
+import type { AlertColor } from '@mui/material/Alert/Alert';
 import type { RootState } from '../store';
 
 export interface SnackbarState {
   open: boolean;
   message: string;
-  severity?: 'success' | 'error';
+  severity?: AlertColor;
 }
 
 const initialState: SnackbarState = {
diff --git a/client/src/store.ts b/client/src/store.ts
index 4814868..e28bace 100644
--- a/client/src/store.ts
+++ b/client/src/store.ts
@@ -1,22 +1,31 @@
-import { configureStore } from '@reduxjs/toolkit';
+import { combineReducers, configureStore } from '@reduxjs/toolkit';
 import guestReducer from './slices/auth/guestSlice';
 import adminReducer from './slices/auth/adminSlice';
 import snackbarReducer from './slices/snackbarSlice';
 import { guestSlice } from './slices/api/guestSlice';
 import { adminSlice } from './slices/api/adminSlice';
 
-const store = configureStore({
-  reducer: {
-    [guestSlice.reducerPath]: guestSlice.reducer,
-    [adminSlice.reducerPath]: adminSlice.reducer,
-    guest: guestReducer,
-    admin: adminReducer,
-    snackbar: snackbarReducer,
-  },
-  middleware: (getDefaultMiddleware) =>
-    getDefaultMiddleware().concat(guestSlice.middleware, adminSlice.middleware),
+const rootReducer = combineReducers({
+  [guestSlice.reducerPath]: guestSlice.reducer,
+  [adminSlice.reducerPath]: adminSlice.reducer,
+  guest: guestReducer,
+  admin: adminReducer,
+  snackbar: snackbarReducer,
 });
 
-export default store;
-export type RootState = ReturnType<typeof store.getState>;
-export type AppDispatch = typeof store.dispatch;
+const setupStore = (preloadedState?: Partial<RootState>) => {
+  return configureStore({
+    reducer: rootReducer,
+    middleware: (getDefaultMiddleware) =>
+      getDefaultMiddleware().concat(
+        guestSlice.middleware,
+        adminSlice.middleware
+      ),
+    preloadedState,
+  });
+};
+
+export default setupStore;
+export type RootState = ReturnType<typeof rootReducer>;
+export type AppStore = ReturnType<typeof configureStore>;
+export type AppDispatch = AppStore['dispatch'];
diff --git a/client/tsconfig.json b/client/tsconfig.json
index f5068c4..4c214ba 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -25,5 +25,5 @@
     "noUnusedParameters": true,
     "noPropertyAccessFromIndexSignature": true
   },
-  "include": ["vite.config.ts"]
+  "include": ["vite-env.d.ts", "vite.config.ts"]
 }
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 5a33944..2942029 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -1,7 +1,13 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+/// <reference types="vitest" />
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [react()],
-})
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: './src/setup.ts',
+  },
+});
diff --git a/server/post.json b/server/post.json
deleted file mode 100644
index f12bd5a..0000000
--- a/server/post.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "id": 1,
-  "firstName": "Michael",
-  "lastName": "Hunteman",
-  "attendance": "yes",
-  "email": "mhunteman@cox.net",
-  "message": "",
-  "partySize": 1,
-  "partyList": []
-}
-- 
cgit v1.2.3