admin管理员组

文章数量:1026182

For the life of me I cannot get simple component testing to work with this setup (Vite, React, TypeScript, Redux-Toolkit and Msal).

Any help would be hugely appreciated.

I have a simple Menu Component below:

//Menu.tsx:

import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Typography, Box, List, ListItemButton, ListItemText, ListItem, Drawer, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';

import AuthButton from '../../services/auth/AuthButton';
import { menuLeftSettings, menuLeftTransactions, menuWidth } from '../../constants/constants';
import { useMediaQueries } from '../../utils/mediaQueries';
import { font } from '../../theme/themeVariables';

const Menu = () => {
  const { isUnderTablet } = useMediaQueries();

  // Using useMemo to memoize menu items for performance optimization
  const menuTransactions = useMemo(() => menuLeftTransactions, []);
  const menuSettings = useMemo(() => menuLeftSettings, []);

  return isUnderTablet ? (
    <MenuDrawer />
  ) : (
    <MenuContent menuWidth={menuWidth} menuTransactions={menuTransactions} menuSettings={menuSettings} />
  );
};

const MenuDrawer = () => {
  const [open, setOpen] = useState(false);
  const handleDrawerOpen = () => setOpen(true);
  const handleDrawerClose = () => setOpen(false);

  return (
    <>
      <IconButton
        color='inherit'
        aria-label='open drawer'
        onClick={handleDrawerOpen}
        sx={[
          {
            position: 'fixed',
            top: 10,
            left: 10,
            zIndex: 100,
          },
          open && { display: 'none' },
        ]}
      >
        <MenuIcon sx={{ width: '1.25em', height: '1.25em' }} />
      </IconButton>
      <Drawer
        sx={{ width: `${menuWidth}px` }}
        variant='temporary'
        anchor='left'
        open={open}
        onClose={handleDrawerClose}
        ModalProps={{
          keepMounted: true, // Better open performance on mobile.
        }}
      >
        <MenuContent menuWidth={menuWidth} menuTransactions={menuLeftTransactions} menuSettings={menuLeftSettings} />
      </Drawer>
    </>
  );
};

interface MenuContent {
  menuWidth: number;
  menuTransactions: { title: string; route: string }[];
  menuSettings: { title: string; route: string }[];
}

const MenuContent = (props: MenuContent) => {
  // Get current location from router to highlight the selected menu item
  const location = useLocation();
  // State to keep track of the selected menu item across all sections
  const [selectedIndex, setSelectedIndex] = useState(location.pathname);

  const handleListItemClick = (key: string) => setSelectedIndex(key);

  return (
    <Box sx={{ bgcolor: 'primary.main', position: 'fixed', width: `${props.menuWidth}px`, height: '100%' }}>
      <MenuSection
        title='Transactions'
        menuItems={menuLeftTransactions}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <MenuSection
        title='Settings'
        menuItems={menuLeftSettings}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <Box sx={{ position: 'absolute', bottom: '80px', left: '16px' }}>
        <AuthButton color={font.menu} />
      </Box>
    </Box>
  );
};

interface MenuSectionProps {
  menuItems: { title: string; route: string }[];
  title: string;
  selectedIndex: string | null;
  onItemClick: (key: string) => void;
}

/* Menu Sections created from the menu constant items - to add menu items, go to the constants and add a menu item. */
const MenuSection = (props: MenuSectionProps) => {
  const { title, menuItems, selectedIndex, onItemClick } = props;

  return (
    <>
      <Typography variant='h4' sx={{ color: 'secondary.dark', pl: 3, pt: 3 }}>
        {title}
      </Typography>
      <List>
        {menuItems.map((item) => (
          <ListItem disablePadding key={item.route}>
            <ListItemButton
              sx={{ pl: 3 }}
              component={Link}
              to={item.route}
              selected={selectedIndex === item.route}
              onClick={() => onItemClick(item.route)}
            >
              <ListItemText disableTypography>{item.title}</ListItemText>
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </>
  );
};

export default Menu;

A Test file below:

//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import createMockStore from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    menuLeftTransactions.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });
  });
});

And I've tried mocking a store as follows:

//mockStore.ts:

// This is purely used for mocking the store in tests. It is not used in the application code.
import configureMockStore from 'redux-mock-store';
import { RootState } from '../../store/store';
import thunk from 'redux-thunk';

// Middleware to match your real store's middleware
const middlewares = [thunk as any];

// Create the mock store with middlewares
const mockStore = configureMockStore<RootState>(middlewares);

// Initial mock state that resembles the actual store
export const getMockState = (): RootState => ({
  auth: {
    user: null,
    token: null,
    expiresAt: null,
  },
  dateSettings: {
    timeZone: { value: '', label: '', abbrev: '', altName: '', offset: 0 },
    dateFormat: 'dd/MM/yyyy hh:mm a',
  },
  transactions: {
    pageNumber: 0,
    pageSize: 0,
    totalCount: 0,
    pageIndex: 0,
    items: [],
    error: false,
    errorMessage: '',
  },
  appSettings: {
    error: false,
    errorMessage: '',
    isLoading: false,
  },
  _persist: {
    version: 1,
    rehydrated: true,
  },
});

// Function to create a new store instance for each test
export const createMockStore = (state: Partial<RootState> = {}) => mockStore({ ...getMockState(), ...state });

export default createMockStore;

setupTests.ts for Vite to use is here:

//setupTests.ts

import * as matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';
import { MsalReactTesterPlugin } from 'msal-react-tester';
import { expect, vi } from 'vitest';
import { waitFor } from '@testing-library/react';

// Use jest-dom with Vitest
expect.extend(matchers);

MsalReactTesterPlugin.init({
  spyOn: vi.spyOn,
  expect: expect,
  resetAllMocks: vi.resetAllMocks,
  waitingFor: waitFor,
});

The Menu.tsx component renders an AuthButton component, which kept referring to and using my authConfig file from Msal, so I was basically unable to mock this.

If anyone has ANY idea on how to actually configure all of this so the tests run, or an easier suggestion on how to setup simple component testing, that would be hugely appreciated.

Errors I receive are anything from the below... TypeError: middleware is not a function configureMockStore is marked as deprecated (and the suggested fix does not help)

I've gone through this step by step but keep bumping into a new error every time I try and run these tests. It's like pulling teeth.

For the life of me I cannot get simple component testing to work with this setup (Vite, React, TypeScript, Redux-Toolkit and Msal).

Any help would be hugely appreciated.

I have a simple Menu Component below:

//Menu.tsx:

import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Typography, Box, List, ListItemButton, ListItemText, ListItem, Drawer, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';

import AuthButton from '../../services/auth/AuthButton';
import { menuLeftSettings, menuLeftTransactions, menuWidth } from '../../constants/constants';
import { useMediaQueries } from '../../utils/mediaQueries';
import { font } from '../../theme/themeVariables';

const Menu = () => {
  const { isUnderTablet } = useMediaQueries();

  // Using useMemo to memoize menu items for performance optimization
  const menuTransactions = useMemo(() => menuLeftTransactions, []);
  const menuSettings = useMemo(() => menuLeftSettings, []);

  return isUnderTablet ? (
    <MenuDrawer />
  ) : (
    <MenuContent menuWidth={menuWidth} menuTransactions={menuTransactions} menuSettings={menuSettings} />
  );
};

const MenuDrawer = () => {
  const [open, setOpen] = useState(false);
  const handleDrawerOpen = () => setOpen(true);
  const handleDrawerClose = () => setOpen(false);

  return (
    <>
      <IconButton
        color='inherit'
        aria-label='open drawer'
        onClick={handleDrawerOpen}
        sx={[
          {
            position: 'fixed',
            top: 10,
            left: 10,
            zIndex: 100,
          },
          open && { display: 'none' },
        ]}
      >
        <MenuIcon sx={{ width: '1.25em', height: '1.25em' }} />
      </IconButton>
      <Drawer
        sx={{ width: `${menuWidth}px` }}
        variant='temporary'
        anchor='left'
        open={open}
        onClose={handleDrawerClose}
        ModalProps={{
          keepMounted: true, // Better open performance on mobile.
        }}
      >
        <MenuContent menuWidth={menuWidth} menuTransactions={menuLeftTransactions} menuSettings={menuLeftSettings} />
      </Drawer>
    </>
  );
};

interface MenuContent {
  menuWidth: number;
  menuTransactions: { title: string; route: string }[];
  menuSettings: { title: string; route: string }[];
}

const MenuContent = (props: MenuContent) => {
  // Get current location from router to highlight the selected menu item
  const location = useLocation();
  // State to keep track of the selected menu item across all sections
  const [selectedIndex, setSelectedIndex] = useState(location.pathname);

  const handleListItemClick = (key: string) => setSelectedIndex(key);

  return (
    <Box sx={{ bgcolor: 'primary.main', position: 'fixed', width: `${props.menuWidth}px`, height: '100%' }}>
      <MenuSection
        title='Transactions'
        menuItems={menuLeftTransactions}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <MenuSection
        title='Settings'
        menuItems={menuLeftSettings}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <Box sx={{ position: 'absolute', bottom: '80px', left: '16px' }}>
        <AuthButton color={font.menu} />
      </Box>
    </Box>
  );
};

interface MenuSectionProps {
  menuItems: { title: string; route: string }[];
  title: string;
  selectedIndex: string | null;
  onItemClick: (key: string) => void;
}

/* Menu Sections created from the menu constant items - to add menu items, go to the constants and add a menu item. */
const MenuSection = (props: MenuSectionProps) => {
  const { title, menuItems, selectedIndex, onItemClick } = props;

  return (
    <>
      <Typography variant='h4' sx={{ color: 'secondary.dark', pl: 3, pt: 3 }}>
        {title}
      </Typography>
      <List>
        {menuItems.map((item) => (
          <ListItem disablePadding key={item.route}>
            <ListItemButton
              sx={{ pl: 3 }}
              component={Link}
              to={item.route}
              selected={selectedIndex === item.route}
              onClick={() => onItemClick(item.route)}
            >
              <ListItemText disableTypography>{item.title}</ListItemText>
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </>
  );
};

export default Menu;

A Test file below:

//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import createMockStore from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    menuLeftTransactions.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });
  });
});

And I've tried mocking a store as follows:

//mockStore.ts:

// This is purely used for mocking the store in tests. It is not used in the application code.
import configureMockStore from 'redux-mock-store';
import { RootState } from '../../store/store';
import thunk from 'redux-thunk';

// Middleware to match your real store's middleware
const middlewares = [thunk as any];

// Create the mock store with middlewares
const mockStore = configureMockStore<RootState>(middlewares);

// Initial mock state that resembles the actual store
export const getMockState = (): RootState => ({
  auth: {
    user: null,
    token: null,
    expiresAt: null,
  },
  dateSettings: {
    timeZone: { value: '', label: '', abbrev: '', altName: '', offset: 0 },
    dateFormat: 'dd/MM/yyyy hh:mm a',
  },
  transactions: {
    pageNumber: 0,
    pageSize: 0,
    totalCount: 0,
    pageIndex: 0,
    items: [],
    error: false,
    errorMessage: '',
  },
  appSettings: {
    error: false,
    errorMessage: '',
    isLoading: false,
  },
  _persist: {
    version: 1,
    rehydrated: true,
  },
});

// Function to create a new store instance for each test
export const createMockStore = (state: Partial<RootState> = {}) => mockStore({ ...getMockState(), ...state });

export default createMockStore;

setupTests.ts for Vite to use is here:

//setupTests.ts

import * as matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';
import { MsalReactTesterPlugin } from 'msal-react-tester';
import { expect, vi } from 'vitest';
import { waitFor } from '@testing-library/react';

// Use jest-dom with Vitest
expect.extend(matchers);

MsalReactTesterPlugin.init({
  spyOn: vi.spyOn,
  expect: expect,
  resetAllMocks: vi.resetAllMocks,
  waitingFor: waitFor,
});

The Menu.tsx component renders an AuthButton component, which kept referring to and using my authConfig file from Msal, so I was basically unable to mock this.

If anyone has ANY idea on how to actually configure all of this so the tests run, or an easier suggestion on how to setup simple component testing, that would be hugely appreciated.

Errors I receive are anything from the below... TypeError: middleware is not a function configureMockStore is marked as deprecated (and the suggested fix does not help)

I've gone through this step by step but keep bumping into a new error every time I try and run these tests. It's like pulling teeth.

Share asked Mar 11 at 0:58 Cactusman07Cactusman07 1,0311 gold badge8 silver badges20 bronze badges 2
  • Modern tests generally use a real Redux store now, using a mock store hasn't been recommended for a couple years now. – Drew Reese Commented Mar 11 at 1:10
  • Ok, thanks. Will update and see if I can fix. – Cactusman07 Commented Mar 11 at 1:20
Add a comment  | 

1 Answer 1

Reset to default 0

I managed to get this sorted by doing the following. I re-wrote 'mockStore' to use @reduxjs/toolkit's configureStore directly, instead of using the deprecated redux-mock-store version.

//mockStore.ts:

import { configureStore } from '@reduxjs/toolkit';
import {
  authReducer,
  timeZoneReducer,
  transactionReducer,
  applicationReducer,
} from '../../store/reducers/actionReducerIndex';

// Define a function to create a mock store
export const createMockStore = (preloadedState = {}) => {
  return configureStore({
    reducer: {
      auth: authReducer,
      dateSettings: timeZoneReducer,
      transactions: transactionReducer,
      appSettings: applicationReducer,
    },
    preloadedState,
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: false, // Disable serialization warnings
      }),
  });
};
```` 

I also updated the tests to use a data-testid in the link elements of the menus. 

````
//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import { createMockStore } from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    const menuItems = screen.getAllByTestId('menu-title');

    expect(menuItems).toHaveLength(menuLeftTransactions.length + menuLeftSettings.length);

    menuLeftTransactions.forEach(({ title }) => {
      expect(menuItems.some((item) => item.textContent === title)).toBeTruthy();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(menuItems.some((item) => item.textContent === title)).toBeTruthy();
    });
  });
});

I've still called it 'mockStore', as it doesn't use redux-persist to persist state to localStorage, but other than that it uses my real store.

For the life of me I cannot get simple component testing to work with this setup (Vite, React, TypeScript, Redux-Toolkit and Msal).

Any help would be hugely appreciated.

I have a simple Menu Component below:

//Menu.tsx:

import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Typography, Box, List, ListItemButton, ListItemText, ListItem, Drawer, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';

import AuthButton from '../../services/auth/AuthButton';
import { menuLeftSettings, menuLeftTransactions, menuWidth } from '../../constants/constants';
import { useMediaQueries } from '../../utils/mediaQueries';
import { font } from '../../theme/themeVariables';

const Menu = () => {
  const { isUnderTablet } = useMediaQueries();

  // Using useMemo to memoize menu items for performance optimization
  const menuTransactions = useMemo(() => menuLeftTransactions, []);
  const menuSettings = useMemo(() => menuLeftSettings, []);

  return isUnderTablet ? (
    <MenuDrawer />
  ) : (
    <MenuContent menuWidth={menuWidth} menuTransactions={menuTransactions} menuSettings={menuSettings} />
  );
};

const MenuDrawer = () => {
  const [open, setOpen] = useState(false);
  const handleDrawerOpen = () => setOpen(true);
  const handleDrawerClose = () => setOpen(false);

  return (
    <>
      <IconButton
        color='inherit'
        aria-label='open drawer'
        onClick={handleDrawerOpen}
        sx={[
          {
            position: 'fixed',
            top: 10,
            left: 10,
            zIndex: 100,
          },
          open && { display: 'none' },
        ]}
      >
        <MenuIcon sx={{ width: '1.25em', height: '1.25em' }} />
      </IconButton>
      <Drawer
        sx={{ width: `${menuWidth}px` }}
        variant='temporary'
        anchor='left'
        open={open}
        onClose={handleDrawerClose}
        ModalProps={{
          keepMounted: true, // Better open performance on mobile.
        }}
      >
        <MenuContent menuWidth={menuWidth} menuTransactions={menuLeftTransactions} menuSettings={menuLeftSettings} />
      </Drawer>
    </>
  );
};

interface MenuContent {
  menuWidth: number;
  menuTransactions: { title: string; route: string }[];
  menuSettings: { title: string; route: string }[];
}

const MenuContent = (props: MenuContent) => {
  // Get current location from router to highlight the selected menu item
  const location = useLocation();
  // State to keep track of the selected menu item across all sections
  const [selectedIndex, setSelectedIndex] = useState(location.pathname);

  const handleListItemClick = (key: string) => setSelectedIndex(key);

  return (
    <Box sx={{ bgcolor: 'primary.main', position: 'fixed', width: `${props.menuWidth}px`, height: '100%' }}>
      <MenuSection
        title='Transactions'
        menuItems={menuLeftTransactions}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <MenuSection
        title='Settings'
        menuItems={menuLeftSettings}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <Box sx={{ position: 'absolute', bottom: '80px', left: '16px' }}>
        <AuthButton color={font.menu} />
      </Box>
    </Box>
  );
};

interface MenuSectionProps {
  menuItems: { title: string; route: string }[];
  title: string;
  selectedIndex: string | null;
  onItemClick: (key: string) => void;
}

/* Menu Sections created from the menu constant items - to add menu items, go to the constants and add a menu item. */
const MenuSection = (props: MenuSectionProps) => {
  const { title, menuItems, selectedIndex, onItemClick } = props;

  return (
    <>
      <Typography variant='h4' sx={{ color: 'secondary.dark', pl: 3, pt: 3 }}>
        {title}
      </Typography>
      <List>
        {menuItems.map((item) => (
          <ListItem disablePadding key={item.route}>
            <ListItemButton
              sx={{ pl: 3 }}
              component={Link}
              to={item.route}
              selected={selectedIndex === item.route}
              onClick={() => onItemClick(item.route)}
            >
              <ListItemText disableTypography>{item.title}</ListItemText>
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </>
  );
};

export default Menu;

A Test file below:

//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import createMockStore from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    menuLeftTransactions.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });
  });
});

And I've tried mocking a store as follows:

//mockStore.ts:

// This is purely used for mocking the store in tests. It is not used in the application code.
import configureMockStore from 'redux-mock-store';
import { RootState } from '../../store/store';
import thunk from 'redux-thunk';

// Middleware to match your real store's middleware
const middlewares = [thunk as any];

// Create the mock store with middlewares
const mockStore = configureMockStore<RootState>(middlewares);

// Initial mock state that resembles the actual store
export const getMockState = (): RootState => ({
  auth: {
    user: null,
    token: null,
    expiresAt: null,
  },
  dateSettings: {
    timeZone: { value: '', label: '', abbrev: '', altName: '', offset: 0 },
    dateFormat: 'dd/MM/yyyy hh:mm a',
  },
  transactions: {
    pageNumber: 0,
    pageSize: 0,
    totalCount: 0,
    pageIndex: 0,
    items: [],
    error: false,
    errorMessage: '',
  },
  appSettings: {
    error: false,
    errorMessage: '',
    isLoading: false,
  },
  _persist: {
    version: 1,
    rehydrated: true,
  },
});

// Function to create a new store instance for each test
export const createMockStore = (state: Partial<RootState> = {}) => mockStore({ ...getMockState(), ...state });

export default createMockStore;

setupTests.ts for Vite to use is here:

//setupTests.ts

import * as matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';
import { MsalReactTesterPlugin } from 'msal-react-tester';
import { expect, vi } from 'vitest';
import { waitFor } from '@testing-library/react';

// Use jest-dom with Vitest
expect.extend(matchers);

MsalReactTesterPlugin.init({
  spyOn: vi.spyOn,
  expect: expect,
  resetAllMocks: vi.resetAllMocks,
  waitingFor: waitFor,
});

The Menu.tsx component renders an AuthButton component, which kept referring to and using my authConfig file from Msal, so I was basically unable to mock this.

If anyone has ANY idea on how to actually configure all of this so the tests run, or an easier suggestion on how to setup simple component testing, that would be hugely appreciated.

Errors I receive are anything from the below... TypeError: middleware is not a function configureMockStore is marked as deprecated (and the suggested fix does not help)

I've gone through this step by step but keep bumping into a new error every time I try and run these tests. It's like pulling teeth.

For the life of me I cannot get simple component testing to work with this setup (Vite, React, TypeScript, Redux-Toolkit and Msal).

Any help would be hugely appreciated.

I have a simple Menu Component below:

//Menu.tsx:

import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Typography, Box, List, ListItemButton, ListItemText, ListItem, Drawer, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';

import AuthButton from '../../services/auth/AuthButton';
import { menuLeftSettings, menuLeftTransactions, menuWidth } from '../../constants/constants';
import { useMediaQueries } from '../../utils/mediaQueries';
import { font } from '../../theme/themeVariables';

const Menu = () => {
  const { isUnderTablet } = useMediaQueries();

  // Using useMemo to memoize menu items for performance optimization
  const menuTransactions = useMemo(() => menuLeftTransactions, []);
  const menuSettings = useMemo(() => menuLeftSettings, []);

  return isUnderTablet ? (
    <MenuDrawer />
  ) : (
    <MenuContent menuWidth={menuWidth} menuTransactions={menuTransactions} menuSettings={menuSettings} />
  );
};

const MenuDrawer = () => {
  const [open, setOpen] = useState(false);
  const handleDrawerOpen = () => setOpen(true);
  const handleDrawerClose = () => setOpen(false);

  return (
    <>
      <IconButton
        color='inherit'
        aria-label='open drawer'
        onClick={handleDrawerOpen}
        sx={[
          {
            position: 'fixed',
            top: 10,
            left: 10,
            zIndex: 100,
          },
          open && { display: 'none' },
        ]}
      >
        <MenuIcon sx={{ width: '1.25em', height: '1.25em' }} />
      </IconButton>
      <Drawer
        sx={{ width: `${menuWidth}px` }}
        variant='temporary'
        anchor='left'
        open={open}
        onClose={handleDrawerClose}
        ModalProps={{
          keepMounted: true, // Better open performance on mobile.
        }}
      >
        <MenuContent menuWidth={menuWidth} menuTransactions={menuLeftTransactions} menuSettings={menuLeftSettings} />
      </Drawer>
    </>
  );
};

interface MenuContent {
  menuWidth: number;
  menuTransactions: { title: string; route: string }[];
  menuSettings: { title: string; route: string }[];
}

const MenuContent = (props: MenuContent) => {
  // Get current location from router to highlight the selected menu item
  const location = useLocation();
  // State to keep track of the selected menu item across all sections
  const [selectedIndex, setSelectedIndex] = useState(location.pathname);

  const handleListItemClick = (key: string) => setSelectedIndex(key);

  return (
    <Box sx={{ bgcolor: 'primary.main', position: 'fixed', width: `${props.menuWidth}px`, height: '100%' }}>
      <MenuSection
        title='Transactions'
        menuItems={menuLeftTransactions}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <MenuSection
        title='Settings'
        menuItems={menuLeftSettings}
        selectedIndex={selectedIndex}
        onItemClick={handleListItemClick}
      />
      <Box sx={{ position: 'absolute', bottom: '80px', left: '16px' }}>
        <AuthButton color={font.menu} />
      </Box>
    </Box>
  );
};

interface MenuSectionProps {
  menuItems: { title: string; route: string }[];
  title: string;
  selectedIndex: string | null;
  onItemClick: (key: string) => void;
}

/* Menu Sections created from the menu constant items - to add menu items, go to the constants and add a menu item. */
const MenuSection = (props: MenuSectionProps) => {
  const { title, menuItems, selectedIndex, onItemClick } = props;

  return (
    <>
      <Typography variant='h4' sx={{ color: 'secondary.dark', pl: 3, pt: 3 }}>
        {title}
      </Typography>
      <List>
        {menuItems.map((item) => (
          <ListItem disablePadding key={item.route}>
            <ListItemButton
              sx={{ pl: 3 }}
              component={Link}
              to={item.route}
              selected={selectedIndex === item.route}
              onClick={() => onItemClick(item.route)}
            >
              <ListItemText disableTypography>{item.title}</ListItemText>
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </>
  );
};

export default Menu;

A Test file below:

//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import createMockStore from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    menuLeftTransactions.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(screen.getByText(title)).toBeInTheDocument();
    });
  });
});

And I've tried mocking a store as follows:

//mockStore.ts:

// This is purely used for mocking the store in tests. It is not used in the application code.
import configureMockStore from 'redux-mock-store';
import { RootState } from '../../store/store';
import thunk from 'redux-thunk';

// Middleware to match your real store's middleware
const middlewares = [thunk as any];

// Create the mock store with middlewares
const mockStore = configureMockStore<RootState>(middlewares);

// Initial mock state that resembles the actual store
export const getMockState = (): RootState => ({
  auth: {
    user: null,
    token: null,
    expiresAt: null,
  },
  dateSettings: {
    timeZone: { value: '', label: '', abbrev: '', altName: '', offset: 0 },
    dateFormat: 'dd/MM/yyyy hh:mm a',
  },
  transactions: {
    pageNumber: 0,
    pageSize: 0,
    totalCount: 0,
    pageIndex: 0,
    items: [],
    error: false,
    errorMessage: '',
  },
  appSettings: {
    error: false,
    errorMessage: '',
    isLoading: false,
  },
  _persist: {
    version: 1,
    rehydrated: true,
  },
});

// Function to create a new store instance for each test
export const createMockStore = (state: Partial<RootState> = {}) => mockStore({ ...getMockState(), ...state });

export default createMockStore;

setupTests.ts for Vite to use is here:

//setupTests.ts

import * as matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';
import { MsalReactTesterPlugin } from 'msal-react-tester';
import { expect, vi } from 'vitest';
import { waitFor } from '@testing-library/react';

// Use jest-dom with Vitest
expect.extend(matchers);

MsalReactTesterPlugin.init({
  spyOn: vi.spyOn,
  expect: expect,
  resetAllMocks: vi.resetAllMocks,
  waitingFor: waitFor,
});

The Menu.tsx component renders an AuthButton component, which kept referring to and using my authConfig file from Msal, so I was basically unable to mock this.

If anyone has ANY idea on how to actually configure all of this so the tests run, or an easier suggestion on how to setup simple component testing, that would be hugely appreciated.

Errors I receive are anything from the below... TypeError: middleware is not a function configureMockStore is marked as deprecated (and the suggested fix does not help)

I've gone through this step by step but keep bumping into a new error every time I try and run these tests. It's like pulling teeth.

Share asked Mar 11 at 0:58 Cactusman07Cactusman07 1,0311 gold badge8 silver badges20 bronze badges 2
  • Modern tests generally use a real Redux store now, using a mock store hasn't been recommended for a couple years now. – Drew Reese Commented Mar 11 at 1:10
  • Ok, thanks. Will update and see if I can fix. – Cactusman07 Commented Mar 11 at 1:20
Add a comment  | 

1 Answer 1

Reset to default 0

I managed to get this sorted by doing the following. I re-wrote 'mockStore' to use @reduxjs/toolkit's configureStore directly, instead of using the deprecated redux-mock-store version.

//mockStore.ts:

import { configureStore } from '@reduxjs/toolkit';
import {
  authReducer,
  timeZoneReducer,
  transactionReducer,
  applicationReducer,
} from '../../store/reducers/actionReducerIndex';

// Define a function to create a mock store
export const createMockStore = (preloadedState = {}) => {
  return configureStore({
    reducer: {
      auth: authReducer,
      dateSettings: timeZoneReducer,
      transactions: transactionReducer,
      appSettings: applicationReducer,
    },
    preloadedState,
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: false, // Disable serialization warnings
      }),
  });
};
```` 

I also updated the tests to use a data-testid in the link elements of the menus. 

````
//Menu.test.tsx

import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import { render, screen } from '@testing-library/react';
import { expect, vi, Mock } from 'vitest';
import { MsalReactTester } from 'msal-react-tester';
import { MsalProvider } from '@azure/msal-react';

// Mock the useMediaQueries hook
vi.mock('../../utils/mediaQueries', () => ({
  useMediaQueries: vi.fn(),
}));

import { useMediaQueries } from '../../utils/mediaQueries';
import { menuLeftTransactions, menuLeftSettings } from '../../constants/constants';
import { createMockStore } from '../../constants/test/mockStore';
import Menu from './Menu';

describe('Menu Component', () => {
  let msalTester: MsalReactTester;

  beforeEach(() => {
    // new instance of MSAL Tester for each test
    msalTester = new MsalReactTester();

    // spy on all required msal functions
    msalTester.spyMsal();
  });

  afterEach(() => {
    vi.clearAllMocks();
    msalTester.resetSpyMsal();
  });

  test('should render the Menu Component when isUnderTablet is false (desktop mode)', async () => {
    const mockUseMediaQueries = useMediaQueries as unknown as Mock;
    mockUseMediaQueries.mockReturnValue({ isUnderTablet: false });

    const store = createMockStore({
      auth: {
        user: {
          given_name: 'John',
          family_name: 'Doe',
          email: '[email protected]',
          accountId: '',
          environment: '',
          city: '',
          country: '',
          authorityType: '',
        },
        token: null,
        expiresAt: null,
      },
    });

    await msalTester.isLogged();

    // MSAL Provider receives msalTester.client as the instance, allowing the test to use the mocked authentication state.
    render(
      <MsalProvider instance={msalTester.client}>
        <Provider store={store}>
          <MemoryRouter>
            <Menu />
          </MemoryRouter>
        </Provider>
      </MsalProvider>,
    );

    // This waits for authentication flow to complete.
    await msalTester.waitForRedirect();

    // Check if transactions menu is rendered
    const menuItems = screen.getAllByTestId('menu-title');

    expect(menuItems).toHaveLength(menuLeftTransactions.length + menuLeftSettings.length);

    menuLeftTransactions.forEach(({ title }) => {
      expect(menuItems.some((item) => item.textContent === title)).toBeTruthy();
    });

    // Check if settings menu is rendered
    menuLeftSettings.forEach(({ title }) => {
      expect(menuItems.some((item) => item.textContent === title)).toBeTruthy();
    });
  });
});

I've still called it 'mockStore', as it doesn't use redux-persist to persist state to localStorage, but other than that it uses my real store.

本文标签: reactjsMsalReactVite amp Reduxcomponent testing unable to workStack Overflow