summaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
authorMichael Hunteman <michael@huntm.net>2024-09-27 08:43:02 -0700
committerMichael Hunteman <michael@huntm.net>2024-09-27 08:43:02 -0700
commita88f613da7e5567dbfdebd7df94f94507c47c6b5 (patch)
treeb10a6c1640c11672a940f8fa71cdf3d3485135d4 /client/src
parent7ccca5ca18200388d10fca33a1d7095a0abfcd36 (diff)
Add vitests
Diffstat (limited to 'client/src')
-rw-r--r--client/src/AppThemeProvider.tsx (renamed from client/src/ThemeContextProvider.tsx)4
-rw-r--r--client/src/components/AdminLogin.test.tsx30
-rw-r--r--client/src/components/Dashboard.tsx1
-rw-r--r--client/src/components/Desktop.tsx2
-rw-r--r--client/src/components/GlobalSnackbar.test.tsx28
-rw-r--r--client/src/components/GuestLogin.test.tsx29
-rw-r--r--client/src/components/Mobile.tsx2
-rw-r--r--client/src/components/RsvpForm.test.tsx31
-rw-r--r--client/src/main.tsx67
-rw-r--r--client/src/mocks/handlers.ts57
-rw-r--r--client/src/mocks/server.ts4
-rw-r--r--client/src/mocks/worker.ts (renamed from client/src/mocks/browser.ts)0
-rw-r--r--client/src/models.ts2
-rw-r--r--client/src/renderWithProviders.tsx28
-rw-r--r--client/src/routes.tsx59
-rw-r--r--client/src/setup.ts6
-rw-r--r--client/src/slices/snackbarSlice.ts3
-rw-r--r--client/src/store.ts37
18 files changed, 289 insertions, 101 deletions
diff --git a/client/src/ThemeContextProvider.tsx b/client/src/AppThemeProvider.tsx
index 5e1e2e8..a88c328 100644
--- a/client/src/ThemeContextProvider.tsx
+++ b/client/src/AppThemeProvider.tsx
@@ -16,7 +16,7 @@ export const ThemeContext = createContext<ThemeContextType>({
toggleColorMode: () => {},
});
-function ThemeContextProvider({ children }: ThemeProviderProps) {
+function AppThemeProvider({ children }: ThemeProviderProps) {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const [mode, setMode] = useState<'light' | 'dark'>(
prefersDarkMode ? 'dark' : 'light'
@@ -91,4 +91,4 @@ function ThemeContextProvider({ children }: ThemeProviderProps) {
);
}
-export default ThemeContextProvider;
+export default AppThemeProvider;
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/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/browser.ts b/client/src/mocks/worker.ts
index 0a56427..0a56427 100644
--- a/client/src/mocks/browser.ts
+++ b/client/src/mocks/worker.ts
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'];