Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-router's useRoute is not correctly mocked #1918

Closed
hidde-jan opened this issue Aug 25, 2022 · 17 comments · Fixed by #1919 or #3491
Closed

vue-router's useRoute is not correctly mocked #1918

hidde-jan opened this issue Aug 25, 2022 · 17 comments · Fixed by #1919 or #3491
Labels

Comments

@hidde-jan
Copy link

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.
@sheremet-va
Copy link
Member

sheremet-va commented Aug 26, 2022

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

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

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

@hidde-jan
Copy link
Author

hidde-jan commented Aug 26, 2022 via email

@sheremet-va
Copy link
Member

sheremet-va commented Aug 26, 2022

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: vuejs/router#1466

@ig-onoffice-de
Copy link

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

@sheremet-va
Copy link
Member

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

@AyuDevs
Copy link

AyuDevs commented Nov 1, 2022

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" }, ], }, }), }));

@sheremet-va
Copy link
Member

sheremet-va commented Nov 1, 2022

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

@AyuDevs
Copy link

AyuDevs commented Nov 1, 2022

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.

@sheremet-va
Copy link
Member

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' })

@AyuDevs
Copy link

AyuDevs commented Nov 2, 2022

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
Copy link

AyuDevs commented Nov 3, 2022

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);

@sheremet-va sheremet-va reopened this Nov 3, 2022
@tim-kilian
Copy link

tim-kilian commented Nov 4, 2022

Same issue. vitest version: ^0.24.5

@jsanchezba
Copy link

jsanchezba commented Jan 3, 2023

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

@jonsalvas
Copy link

works for me with

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

@SSShooter
Copy link

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' })

@ArnoldPMolenaar
Copy link

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 :)

@tomsdob
Copy link

tomsdob commented Jun 15, 2023

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();
  });
});

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
10 participants