vitest icon indicating copy to clipboard operation
vitest copied to clipboard

vue-router's useRoute is not correctly mocked

Open hidde-jan opened this issue 2 years ago • 4 comments

Describe the bug

When using vitest-suggested testing libraries, I can't successfully mock useRoute and useRouter from the vue-router package.

I'm using:

  • jsdom
  • @testing-library/vue

Any time I run a component test with a mocked useRoute, the mock returns undefined, and Vue warns about missing injections.

...
[Vue warn]: injection "Symbol(route location)" not found. 
  at <HelloWorld ref="VTU_COMPONENT" > 
  at <VTUROOT>
...
TypeError: Cannot read properties of undefined (reading 'path')
     19| 
     20|   <div class="card">
     21|     <button type="button" @click="count++">count is {{ count }}</button>
       |                     ^
     22|     <p>
     23|       Edit

Reproduction

See a minimal reproduction, using a fresh yarn create vite project here: https://github.com/hidde-jan/vitest-use-route-example/blob/main/src/components/test/HelloWorld.test.ts

System Info

System:
    OS: macOS 12.3.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 76.22 MB / 32.00 GB
    Shell: 3.3.1 - /opt/homebrew/bin/fish
  Binaries:
    Node: 16.14.0 - ~/.nodenv/versions/16.14.0/bin/node
    Yarn: 1.22.15 - ~/.nodenv/versions/16.14.0/bin/yarn
    npm: 8.3.1 - ~/.nodenv/versions/16.14.0/bin/npm
  Browsers:
    Chrome: 104.0.5112.101
    Firefox: 102.0.1
    Safari: 15.4
  npmPackages:
    @vitejs/plugin-vue: ^3.0.3 => 3.0.3 
    vite: ^3.0.7 => 3.0.9 
    vitest: ^0.22.1 => 0.22.1 


### Used Package Manager

yarn

### Validations

- [X] Follow our [Code of Conduct](https://github.com/vitest-dev/vitest/blob/main/CODE_OF_CONDUCT.md)
- [X] Read the [Contributing Guidelines](https://github.com/vitest-dev/vitest/blob/main/CONTRIBUTING.md).
- [X] Read the [docs](https://vitest.dev/guide/).
- [X] Check that there isn't [already an issue](https://github.com/vitest-dev/vitest/issues) that reports the same bug to avoid creating a duplicate.
- [X] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions) or join our [Discord Chat Server](https://chat.vitest.dev).
- [X] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.

hidde-jan avatar Aug 25 '22 17:08 hidde-jan

Should be fixed by https://github.com/vitest-dev/vitest/issues/1919 and https://github.com/vitejs/vite/pull/9860

For now, you can manually add node condition to your config:

{
  resolve: {
    conditions: process.env.VITEST ? ['node'] : []
  }
}

sheremet-va avatar Aug 26 '22 06:08 sheremet-va

Thanks. I can confirm this actually makes the mock work.

Any specific reason why this bug occurs? It has something to do with module resolution?

Verstuurd vanaf mijn iPhone

Op 26 aug. 2022 om 08:59 heeft Vladimir @.***> het volgende geschreven:

 Should be fixed by #1918 and vitejs/vite#9860

For now, you can manually add node condition to your config:

{ resolve: { conditions: process.env.VITEST ? ['node'] : [] } } — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.

hidde-jan avatar Aug 26 '22 08:08 hidde-jan

Thanks. I can confirm this actually makes the mock work. Any specific reason why this bug occurs? It has something to do with module resolution?

Yes. Vite uses browser field, if vue router is imported from Vue file. But doesn't use it, if it was imported from .ts filed (or mocked inside .ts file). There is also an open issues in router repo: https://github.com/vuejs/router/issues/1466

sheremet-va avatar Aug 26 '22 08:08 sheremet-va

For me this workaround doesn't work. Current Versions: @vitejs/plugin-vue: 3.1.0 vite: 3.1.3 vitest: 0.23.4

ig-onoffice-de avatar Sep 20 '22 07:09 ig-onoffice-de

Should be fixed in the next Vitest version. Also requires Vite 3.2.

sheremet-va avatar Oct 28 '22 15:10 sheremet-va

Should be fixed in the next Vitest version. Also requires Vite 3.2.

I'm running vitest 0.24.4 and vite 3.2.2, and yet the mock is not working. Is the issue really solved?

Mocking with fn() gives "Cannot read properties of undefined (reading 'meta')" error when reading "route.meta.breadcrumbs": vi.mock("vue-router", () => ({ useRoute: vi.fn(() => ({ name: "organisation", query: { org: "" }, path: "/organisations/100047", meta: { breadcrumbs: [ { text: "Home", ref: "/" }, { text: "Search", ref: "/organisation-search" }, { text: "", ref: "/organisations/100047" }, ], }, })), }));

Mocking without fn() works in first test, but overrides mock definition in the next test executed: vi.mock("vue-router", () => ({ useRoute: () => ({ name: "organisation", query: { org: "" }, path: "/organisations/100047", meta: { breadcrumbs: [ { text: "Home", ref: "/" }, { text: "Search", ref: "/organisation-search" }, { text: "", ref: "/organisations/100047" }, ], }, }), }));

AyuDevs avatar Nov 01 '22 16:11 AyuDevs

Yes, it is fixed. If it wasn't, your mocks would never be called. You have error in your test code.

sheremet-va avatar Nov 01 '22 16:11 sheremet-va

Yes, it is fixed. If it wasn't, your mocks would never be called. You have error in your test code.

I have tried all sorts of cleaning/restoring functions before and after each test, but they don't do anything. Mock from the first test overrides the mock definition in the next test executed. At this point, we have to put each test in a separated file to make it work, which is very messy. Is it per design that mocking router once in the file, overrides all later router mocks? Any pointers would be very helpful.

AyuDevs avatar Nov 01 '22 16:11 AyuDevs

I have tried all sorts of cleaning/restoring functions before and after each test, but they don't do anything. Mock from the first test overrides the mock definition in the next test executed. At this point, we have to put each test in a separated file to make it work, which is very messy. Is it per design that mocking router once in the file, overrides all later router mocks? Any pointers would be very helpful.

vi.mock calls are hoisted, it is mention in the documentation. There is a cheat sheet in docs that might be useful: https://vitest.dev/guide/mocking.html#cheat-sheet

The easiest way to mock a route for different tests would be, in my opinion:

import * as routerExports from 'vue-router'
const useRouteMock = vi.spyOn(routerExports, 'useRoute')
useRouteMock.mockReturnValue({ name: 'name1' })
useRouteMock.mockReturnValue({ name: 'name2' })

Or if you like vi.mock so much, you can mark is as a spy in vi.mock:

import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({ useRoute: vi.fn() }))

vi.mocked(useRoute).mockReturnValue({ name: 'name1' })
vi.mocked(useRoute).mockReturnValue({ name: 'name2' })

sheremet-va avatar Nov 01 '22 16:11 sheremet-va

I have tried all sorts of cleaning/restoring functions before and after each test, but they don't do anything. Mock from the first test overrides the mock definition in the next test executed. At this point, we have to put each test in a separated file to make it work, which is very messy. Is it per design that mocking router once in the file, overrides all later router mocks? Any pointers would be very helpful.

vi.mock calls are hoisted, it is mention in the documentation. There is a cheat sheet in docs that might be useful: https://vitest.dev/guide/mocking.html#cheat-sheet

The easiest way to mock a route for different tests would be, in my opinion:

import * as routerExports from 'vue-router'
const useRouteMock = vi.spyOn(routerExports, 'useRoute')
useRouteMock.mockReturnValue({ name: 'name1' })
useRouteMock.mockReturnValue({ name: 'name2' })

Or if you like vi.mock so much, you can mark is as a spy in vi.mock:

import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({ useRoute: vi.fn() }))

vi.mocked(useRoute).mockReturnValue({ name: 'name1' })
vi.mocked(useRoute).mockReturnValue({ name: 'name2' })

Hello again, I've tried both of your examples, and nothing works.


import * as routerExports from "vue-router";

vi.spyOn(routerExports, "useRoute").mockReturnValue({
      name: "organisation",
      query: { org: "" },
      path: "/organisations/100047",
      meta: {
        breadcrumbs: [
          { text: "Home", ref: "/" },
          { text: "Search", ref: "/organisation-search" },
          { text: "", ref: "/organisations/100047" },
        ],
      },
      matched: [],
      fullPath: "",
      hash: "",
      redirectedFrom: undefined,
      params: {},
    });

This is the error I'm getting:

TypeError: Cannot assign to read only property 'useRoute' of object '[object Module]'
 ❯ src/components/Breadcrumbs/Breadcrumbs.test.ts:31:7
     29|     const wrapper = breadcrumbsFactory();
     30| 
     31|     vi.spyOn(routerExports, "useRoute").mockReturnValue({
       |       ^
     32|       name: "organisation",
     33|       query: { org: "" },

[Vue warn]: injection "Symbol(route location)" not found.
  at <Breadcrumbs ref="VTU_COMPONENT" >
  at <VTUROOT>

AyuDevs avatar Nov 02 '22 08:11 AyuDevs

Please reopen this case, the bug hasn't been fixed. @sheremet-va

Still getting:

[Vue warn]: injection "Symbol(route location)" not found.
  at <XenaBreadcrumbs ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: Unhandled error during execution of render function
  at <XenaBreadcrumbs ref="VTU_COMPONENT" >
  at <VTUROOT>
TypeError: Cannot read properties of undefined (reading 'meta')
 ❯ ReactiveEffect.fn src/components/Breadcrumbs/XenaBreadcrumbs.vue:17:19
     15|   const route = useRoute();
     16|   console.log(route);
     17|   const br = route.meta.breadcrumbs as JSON;
       |                   ^
     18| 
     19|   if (route.name === "organisation") {

import * as routerExports from "vue-router";
const mockedUseRoute = {
      name: "organisation",
      query: { org: "" },
      path: "/organisations/100047",
      meta: {
        breadcrumbs: [
          { text: "Home", ref: "/" },
          { text: "Search", ref: "/organisation-search" },
          { text: "", ref: "/organisations/100047" },
        ],
      },
      matched: [],
      fullPath: "",
      hash: "",
      redirectedFrom: undefined,
      params: {},
    };

    const useRouteMock = vi
      .spyOn(routerExports, "useRoute")
      .mockImplementation(() => mockedUseRoute);

AyuDevs avatar Nov 03 '22 13:11 AyuDevs

Same issue. vitest version: ^0.24.5

tim-kilian avatar Nov 04 '22 11:11 tim-kilian

Updated: I did manage to make it work with vi.mock This is what i did for anyone using quasar:

    "overrides": {
        "@vitejs/plugin-vue": "^4.0.0",
        "vite": "^4.0.3",
        "vitest": "^0.26.3"
    }
const mockPush = vi.fn();

vi.mock('vue-router', () => ({
    useRouter: () => ({
        push: mockPush,
        currentRoute: { value: 'myCurrentRoute' },
    }),
}));

This is the error I'm getting:

TypeError: Cannot assign to read only property 'useRoute' of object '[object Module]'
 ❯ src/components/Breadcrumbs/Breadcrumbs.test.ts:31:7
     29|     const wrapper = breadcrumbsFactory();
     30| 
     31|     vi.spyOn(routerExports, "useRoute").mockReturnValue({
       |       ^
     32|       name: "organisation",
     33|       query: { org: "" },

[Vue warn]: injection "Symbol(route location)" not found.
  at <Breadcrumbs ref="VTU_COMPONENT" >
  at <VTUROOT>

I'm facing the same issue as you. I was trying to spy the useRouter() but it trows: Cannot assign to read only property 'useRoute' of object

Also tried with vi.mock and vue-router-mock library, none of those options did work for me

I'm using: Vue 3 composition api Quasar v2

jsanchezba avatar Jan 03 '23 09:01 jsanchezba

works for me with

 "@vitejs/plugin-vue": "^4.0.0",
        "vite": "^4.0.3",
        "vitest": "^0.28.1"

jonsalvas avatar Jan 24 '23 19:01 jonsalvas

I have tried all sorts of cleaning/restoring functions before and after each test, but they don't do anything. Mock from the first test overrides the mock definition in the next test executed. At this point, we have to put each test in a separated file to make it work, which is very messy. Is it per design that mocking router once in the file, overrides all later router mocks? Any pointers would be very helpful.

vi.mock calls are hoisted, it is mention in the documentation. There is a cheat sheet in docs that might be useful: https://vitest.dev/guide/mocking.html#cheat-sheet

The easiest way to mock a route for different tests would be, in my opinion:

import * as routerExports from 'vue-router'
const useRouteMock = vi.spyOn(routerExports, 'useRoute')
useRouteMock.mockReturnValue({ name: 'name1' })
useRouteMock.mockReturnValue({ name: 'name2' })

Or if you like vi.mock so much, you can mark is as a spy in vi.mock:

import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({ useRoute: vi.fn() }))

vi.mocked(useRoute).mockReturnValue({ name: 'name1' })
vi.mocked(useRoute).mockReturnValue({ name: 'name2' })

I got error when I used the first solution

TypeError: Cannot redefine property: useRoute
 ❯ src/__tests__/components/pipelineTemplate/PipelineTemplateTable.spec.ts:11:25
      9| // import { useRoute } from 'vue-router'
     10| import * as routerExports from 'vue-router'
     11| const useRouteMock = vi.spyOn(routerExports, 'useRoute')
       |                         ^
     12| // useRouteMock.mockReturnValue({ name: 'name1' })
     13| // useRouteMock.mockReturnValue({ name: 'name2' })

SSShooter avatar May 06 '23 07:05 SSShooter

I'm experiencing the same issue on version 0.31.4.

See my example of the issue:

/* vitest.config.mjs */
import { defineVitestConfig } from 'nuxt-vitest/config'

export default defineVitestConfig({
  test: {
    globals: true,
    clearMocks: true,
    restoreMocks: true,
    threads: false,
    testTimeout: 300000,
    setupFiles: ['tests/unit.i18n.setup.ts', 'tests/unit.vuetify.setup.ts', 'tests/unit.router.setup.ts'],
    deps: {
      inline: ['element-plus', /@nuxt\/test-utils/],
    },
    ssr: {
      noExternal: [/vue-i18n/, 'vuetify'],
    }
  }
});
/* unit.router.setup.ts */
import { vi } from 'vitest';

vi.mock('vue-router', async (importOriginal) => {
  const mod: object = await importOriginal();

  return {
    ...mod,
    useRouter: vi.fn(() => ({ replace: vi.fn() })),
  };
});
/* component.test.ts */

describe('v-select-locale', async () => {
  const router = createRouterMock();

  beforeEach(() => {
    injectRouterMock(router);
  });
  injectRouterMock(router);

  type ComponentProps = any;
  type ComponentVariables = {
    availableLocales: ComputedRef<SelectItem<string>[]>;
    currentLocale: Ref<string>;
    onChangeLocale: (locale: string) => void;
  };
  type ComponentWrapperType = VueWrapper<ComponentPublicInstance<ComponentProps, ComponentVariables>>;

  const component: ComponentWrapperType = mount(VSelectLocale, {
    global: {
      stubs: ['v-list-item', 'v-select'],
    },
  });

  it('should change the selected locale', () => {
    // This works...., So the router is in the component for testing
    expect(component.router).toBe(router);

    // Error triggered here
    component.vm.onChangeLocale('en');
  });
});

error:

 FAIL  tests/components/v-select-locale.test.ts > v-select-locale > should change the selected locale
TypeError: Cannot read properties of undefined (reading 'replace')
 ❯ Proxy.onChangeLocale components/v-select-locale.vue:63:10
     61|  */
     62| function onChangeLocale(locale: string) {
     63|   router.replace(switchLocalePath(locale));
       |          ^
     64| }
     65| 
 ❯ tests/components/v-select-locale.test.ts:48:5

I would expect that the setup file would mock the vue-router imports with vi.mock @ unit.router.setup.ts If i'm doing something wrong I would like to know what :)

ArnoldPMolenaar avatar Jun 02 '23 08:06 ArnoldPMolenaar

I struggled with this while working on a Nuxt 3 project using Vitest, nuxt-vitest. While tinkering around I stumbled upon a combination that works for me. Here is what worked for me and maybe it helps anyone else:

vi.mock('vue-router', () => ({
  useRoute: vi.fn(() => ({
    fullPath: '',
    hash: '',
    matched: [],
    meta: {},
    name: undefined,
    params: {
      test: '123',
    },
    path: '/test',
    query: {},
    redirectedFrom: undefined,
  })),
}));

My *.spec.ts file uses // @vitest-environment nuxt and mounts the component with mountSuspended from nuxt-vitest as well as auto imports the useRoute from vue-router automatically.

Here is the full *.spec.ts:

// @vitest-environment nuxt
import { describe, expect, it, vi } from 'vitest';
import { mountSuspended } from 'vitest-environment-nuxt/utils';
import Test from '../[[test]].vue';

vi.mock('vue-router', () => ({
  useRoute: vi.fn(() => ({
    fullPath: '',
    hash: '',
    matched: [],
    meta: {},
    name: undefined,
    params: {
      test: '123',
    },
    path: '/test',
    query: {},
    redirectedFrom: undefined,
  })),
}));

describe('Test', async () => {
  const wrapper = await mountSuspended(Test, {});

  it('should render correctly', () => {
    expect(wrapper.html()).toMatchSnapshot();
  });
});

tomsdob avatar Jun 15 '23 07:06 tomsdob