Vitest's 2 different beforeEach methods affect each other in different describe blocks

203 Views Asked by At

I have a React component which uses NextJS useRouter and changes the locale and the language of my web application. I want to write the different aspects of my component with different locales. I have two different describe blocks but the second beforeEach is apparently affecting my first beforeEach mocking.

This is my React component:

import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

type LangProps = 'en' | 'fa';

const LanguagesList = () => {
  const router = useRouter();
  const { pathname, asPath, query, locale } = router;
  const { t: translate } = useTranslation('navbar');
  const [lang, setLang] = useState<LangProps>('en');

  useEffect(() => {
    setLang(locale as LangProps);
  }, [locale]);

  const changLangHandler = (lngValue: LangProps) => {
    setLang(lngValue);

    router.push({ pathname, query }, asPath, {
      locale: lngValue,
    });
  };

  // change just the locale and maintain all other route information including href's query
  return (
    <ul className='flex w-24 flex-col'>
      <li
        className={`relative cursor-pointer text-lg ${
          lang === 'en' &&
          'after:absolute after:-left-3 after:top-0 after:inline-block after:h-full after:w-1 after:bg-accent'
        }`}
        onClick={() => changLangHandler('en')}
      >
        {translate('English')}
      </li>
      <li
        className={`relative cursor-pointer text-lg ${
          lang === 'fa' &&
          'after:absolute after:-right-3 after:top-0 after:inline-block after:h-full after:w-1 after:bg-accent'
        }`}
        onClick={() => changLangHandler('fa')}
      >
        {translate('Farsi')}
      </li>
    </ul>
  );
};

export default LanguagesList;

My test suite:

/* eslint-disable no-empty-function */
import {
  fireEvent,
  render,
  screen,
} from '@testing-library/react';
import { beforeEach, expect, vi } from 'vitest';
import LanguagesList from './LanguagesList';

describe('Languages List Component Tests When Locale is en', () => {
  beforeEach(() => {
    vi.mock('next/router', () => ({
      useRouter() {
        return {
          route: '/',
          pathname: '',
          query: '',
          asPath: '',
          locale: 'en',
          push: vi.fn(),
        };
      },
    }));
  });

  it('should render the component properly initially', () => {
    render(<LanguagesList />);

    const languagesListEl = screen.getByRole('list');

    expect(languagesListEl).toBeInTheDocument();
  });

  it('should render list items with length of 2', () => {
    render(<LanguagesList />);

    const langListItemEls = screen.getAllByRole('listitem');

    langListItemEls.map((item) => {
      expect(item).toBeInTheDocument();
    });

    expect(langListItemEls[0].textContent).toBe('English');
    expect(langListItemEls[1].textContent).toBe('Farsi');
  });

  it('should render the correct styles for the first li element when lang is set to en', () => {
    render(<LanguagesList />);

    const langListItemEls = screen.getAllByRole('listitem');
    langListItemEls[0].getAttribute('class');

    screen.debug();
    expect(langListItemEls[0]).toHaveClass('after:absolute');

    fireEvent.click(langListItemEls[1]);

    // when clicked it should not have the active classes
    expect(langListItemEls[0]).not.toHaveClass('after:absolute');
  });
});

describe('Languages List Component Tests When Locale is fa', () => {
  beforeEach(() => {
    vi.mock('next/router', () => ({
      useRouter() {
        return {
          route: '/',
          pathname: '',
          query: '',
          asPath: '',
          locale: 'fa',
          push: vi.fn(),
        };
      },
    }));
  });

  it('should render the correct styles for the first li element when lang is set to fa', () => {
    render(<LanguagesList />);

    const langListItemEls = screen.getAllByRole('listitem');
    langListItemEls[0].getAttribute('class');

    expect(langListItemEls[0]).not.toHaveClass('after:absolute');
    expect(langListItemEls[1]).toHaveClass('after:absolute');

    // click on en button
    fireEvent.click(langListItemEls[0]);

    // when clicked it should not have the active classes
    expect(langListItemEls[0]).toHaveClass('after:absolute');
    expect(langListItemEls[1]).not.toHaveClass('after:absolute');
  });
});

I did some researches and found out that vi.mock() will be hoisted, so I think the second beforeEach will replace the first one and this is why it makes my first describe block tests fail. I tried vi.doMock but it did not work. I want to know what I'm doing wrong in case of mocking the useRouter or anything else.

1

There are 1 best solutions below

0
On

you can use vi.mock along with vi.hoisted

import { assert } from 'vitest'
import { getTimeFactory } from './getTime.ts'

const mocks = vi.hoisted(() => {
  return { getTime: vi.fn() }
})

vi.mock('./getTime.ts', () => {
  return {
    getTimeFactory: () => mocks.getTime,
  }
})

mocks.getTime.mockReturnValue(0)
const getTime = getTimeFactory()
assert.equals(getTime(), 0)

or you could use vi.doMock and dynamic imports

vi.doMock('./getTime.ts', () => {
  return { getTime: vi.fn() }
})
const mod = await import('./getTime.ts')
// `mod.getTime` will be mocked