6

i'm learning Vue 3 using composition api, and im very new to Vitest aswell (i know it uses Vue Test Utils though).

So, in short i'm having a problem where I mock the Vue Router, trigger click on a router-link element and expect the router to have been called, but it fails.

First of all and just in case, these are the versions I'm using on the dependencies that might be relevant for this matter:

"dependencies": {
  "vue": "^3.3.2",
  "vue-router": "^4.2.0"
},
"devDependencies": {
  "@vitejs/plugin-vue": "^4.2.3",
  "@vitejs/plugin-vue-jsx": "^3.0.1",
  "@vue/test-utils": "^2.3.2",
  "jsdom": "^22.0.0",
  "start-server-and-test": "^2.0.0",
  "vite": "^4.3.5",
  "vitest": "^0.31.0"
}

I have a simple view to test this:

TestView.vue

<template>
  <router-link data-test="test-link" to="login">test link</router-link>
</template>

And I have tried several ways to make the test but here go the 2 that I think would make more sense:

1 - With mockImplementationOnce, from VTU Docs

testView.test.js

import { describe, expect, test, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { useRouter } from 'vue-router'
import TestView from '../../views/TestView.vue'

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

describe('Test View', () => {
  test('Login button redirects to login view', async () => {
    const push = vi.fn()
    useRouter.mockImplementationOnce(() => ({
      push
    }))

    const wrapper = mount(TestView, {
      global: {
        stubs: ['router-link'] // tried removing this stub aswell
      }
    })

    const btn = wrapper.get('[data-test="test-link"]')
    await btn.trigger('click')
    // Few things to note here:
    // I also tried adding await nextTick() and even a promise with 1s timeout to make sure
    // the button is clicked before the assertion happens, but doesn't help.
    
    // Also it's not like the button isn't found, the following assert succeeds
    expect(btn.attributes('to')).toEqual('login')

    // Output: AssertionError: expected "spy" to be called at least once
    expect(push).toHaveBeenCalled()
  })
})


2- With mockReturnValue, saw in this article

testView.test.js

import { beforeEach, describe, expect, test, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { useRouter } from 'vue-router'
import TestView from '../../views/TestView.vue'

vi.mock('vue-router')

describe('Test View', () => {
  useRouter.mockReturnValue({
    push: vi.fn()
  })

  beforeEach(() => {
    useRouter().push.mockReset()
  })
  test('Login button redirects to login view', async () => {
    const wrapper = mount(TestView, {
      global: {
        stubs: ['router-link'] // tried removing this stub aswell
      }
    })

    const btn = wrapper.get('[data-test="test-link"]')
    await btn.trigger('click')
    // Few things to note here:
    // I also tried adding await nextTick() and even a promise with 1s timeout to make sure
    // the button is clicked before the assertion happens, but doesn't help.
    
    // Also it's not like the button isn't found, the following assert succeeds
    expect(btn.attributes('to')).toEqual('login')

    // Output: AssertionError: expected "spy" to be called at least once
    expect(useRouter().push).toHaveBeenCalled()
  })
})

So, in both cases I get the same error: AssertionError: expected "spy" to be called at least once

Every other potential solutions I've found are neither working for me, many of them are most likely outdated.
Anyone sees what I'm missing or if there is another way to do this?

Thank you in advance and sorry if I didn't fully follow the guidelines, it's the first time I ask something here.

1
  • I'm facing the same issue, I'm using your version 1. and I also found the article from version 2 and tried that, too :D I was even able to verify that my mocked function is in fact called by the code.
    – Julisch
    Commented Oct 28, 2023 at 9:35

1 Answer 1

3

I was facing the same issue and finally found a way to make it work. Let me know if it works for you, too:

import { afterEach, describe, expect, it, vi } from 'vitest'
import { render } from '@/test-utils'
import userEvent from '@testing-library/user-event'

afterEach(() => {
  vi.clearAllMocks()
})

const mockRoutePush = vi.fn()
vi.mock('vue-router', async () => {
  return {
    RouterView: {},
    useRouter: () => {
      return {
        push: mockRoutePush
      }
    }
  }
})

describe('when ...', () => {
  it('navigates to ...', async () => {
    // set up everything
    const { screen } = render(MyComponent)

    // click on button
    const user = userEvent.setup()
    await user.click(screen.getByRole('button', { name: 'navigate' }))

    // expect that navigation happened
    expect(mockRoutePush).toHaveBeenCalled()
    expect(mockRoutePush).toHaveBeenCalledWith(
      expect.objectContaining({ name: 'route-name' })
    )
  })
})
1
  • 1
    Thank you you are a life saver. I got stuck with the similar issue and your way actually fixed the issue with router.
    – Ooto
    Commented Mar 1 at 6:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.