vuejs / vue-test-utils

Component Test Utils for Vue 2
https://vue-test-utils.vuejs.org
MIT License
3.57k stars 670 forks source link

useSlots() make component not mountable #2033

Open nemeros opened 1 year ago

nemeros commented 1 year ago

Subject of the issue

Given a basic component, with setup script, if we use the function useSlots(), the component cannot be rendered with mount.

Steps to reproduce

I created a brand new project with create-vue@2, with typescript and vitest :

PS D:\dev\cfdp> npm create vue@2

Vue.js - The Progressive JavaScript Framework

√ Project name: ... vue-test2
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add Cypress for End-to-End testing? ... No / Yes
√ Add ESLint for code quality? ... No / Yes

Now the basic component, SlotComponent.vue :

<template>
  <div>
    <slot name="body"></slot>
  </div>
</template>
<script setup lang="ts">
import { useSlots } from 'vue';

const slots = useSlots();
</script>

The associated test, SlotComponent.spec.ts :

import { describe, expect, it } from 'vitest';
import { mount } from '@vue/test-utils';
import SlotComponent from './SlotComponent.vue'

describe('Slot test', () => {

  it('will fail', () => {
    const wrapper = mount(SlotComponent, {
      slots: {
        body: '<div>body</div>'
      }
    });

    expect(wrapper.text()).toContain('body');
  });
});

Expected behaviour

the test should pass, (it pass if i remove the code const slots = useSlots())

Actual behaviour

the test fail with the stacktrace below :

stderr | src/components/__tests__/SlotComponent.spec.ts > Slot test > will fail
[Vue warn]: useContext() called without active instance.
[Vue warn]: Error in setup: "TypeError: Cannot read properties of null (reading '_setupContext')"

found in

---> <SlotComponent> at D:/dev/cfdp/vue-test/src/components/__tests__/SlotComponent.vue
       <Root>
TypeError: Cannot read properties of null (reading '_setupContext')
    at getContext (D:/dev/cfdp/vue-test/node_modules/vue/dist/vue.runtime.esm.js:2589:15)        
    at Module.useSlots (D:/dev/cfdp/vue-test/node_modules/vue/dist/vue.runtime.esm.js:2567:12)   
    at setup (D:/dev/cfdp/vue-test/src/components/__tests__/SlotComponent.vue:8:41)
    at invokeWithErrorHandling (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:2919:30)
    at initSetup (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:2352:29)
    at initState (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:5300:5)
    at VueComponent.Vue._init (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:5615:9)
    at new VueComponent (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:5750:18)
    at createComponentInstanceForVnode (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:4490:12)
    at init (D:\dev\cfdp\vue-test\node_modules\vue\dist\vue.runtime.common.dev.js:4352:54)

 ❯ src/components/__tests__/SlotComponent.spec.ts (1)
   ❯ Slot test (1)
     × will fail
 ✓ src/components/__tests__/HelloWorld.spec.ts (1)

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/components/__tests__/SlotComponent.spec.ts > Slot test > will fail
TypeError: Cannot read properties of null (reading '_setupContext')
 ❯ getContext node_modules/vue/dist/vue.runtime.esm.js:2589:15
    2587|     }
    2588|     var vm = currentInstance;
    2589|     return vm._setupContext || (vm._setupContext = createSetupContext(vm));
       |               ^
    2590| }
    2591| /**
 ❯ Module.useSlots node_modules/vue/dist/vue.runtime.esm.js:2567:12
 ❯ setup src/components/__tests__/SlotComponent.vue:8:41
 ❯ invokeWithErrorHandling node_modules/vue/dist/vue.runtime.common.dev.js:2919:30
 ❯ initSetup node_modules/vue/dist/vue.runtime.common.dev.js:2352:29
 ❯ initState node_modules/vue/dist/vue.runtime.common.dev.js:5300:5
 ❯ VueComponent.Vue._init node_modules/vue/dist/vue.runtime.common.dev.js:5615:9
 ❯ new VueComponent node_modules/vue/dist/vue.runtime.common.dev.js:5750:18
 ❯ createComponentInstanceForVnode node_modules/vue/dist/vue.runtime.common.dev.js:4490:12
 ❯ init node_modules/vue/dist/vue.runtime.common.dev.js:4352:54

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files  1 failed | 1 passed (2)
     Tests  1 failed | 1 passed (2)
      Time  3.70s (in thread 64ms, 5780.13%)

Annex :

my package.json :

{
  "name": "vue-test",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check build-only",
    "preview": "vite preview --port 4173",
    "test:unit": "vitest --environment jsdom",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false"
  },
  "dependencies": {
    "vue": "^2.7.7"
  },
  "devDependencies": {
    "@types/jsdom": "^16.2.14",
    "@types/node": "^16.11.45",
    "@vitejs/plugin-legacy": "^2.0.0",
    "@vitejs/plugin-vue2": "^1.1.2",
    "@vitejs/plugin-vue2-jsx": "^1.0.2",
    "@vue/test-utils": "^1.3.0",
    "@vue/tsconfig": "^0.1.3",
    "jsdom": "^20.0.0",
    "npm-run-all": "^4.1.5",
    "terser": "^5.14.2",
    "typescript": "~4.7.4",
    "vite": "^3.0.2",
    "vitest": "^0.18.1",
    "vue-template-compiler": "^2.7.7",
    "vue-tsc": "^0.38.8"
  }
}
treardon17 commented 1 year ago

In addition to useSlots, this also happens with useAttrs, useListeners, and getCurrentInstance (using "vue": "~2.7.13" and "@vue/test-utils" : "1.3.x"). I cannot run unit tests while using script setup in combination with any one of those methods. Currently using vitest 0.23.4 as the test runner. Also worth noting this happens with mount as well as shallowMount.

rossinek commented 1 year ago

Quick workaround for vitest (fixes useListeners usage):

// tests setup

vi.mock('vue', async () => {
  const actualModule = await vi.importActual('vue');
  return {
    ...actualModule,
    useListeners: actualModule.default.useListeners,
  };
});
piktur commented 1 year ago

@rossinek how does this solve the issue? This mock does not change the implementation of useListeners. The private module variable currentInstance referenced in getContext will still be undefined.

Unless mocking prevents inconsistent module resolution. Whilst debugging I've noticed both vitest vue/dist/vue.js and vue/dist/vue.esm.js are loaded.

rossinek commented 1 year ago

@piktur thank for taking a look at this. It was a long time ago and TBH I don't remember why it works and currently I don't have the capacity to debug it once again but I created a little reproduction for you if you want to dive in. To see the difference run:

npm run test src/components/HelloWorld.broken.spec.js
# vs
npm run test src/components/HelloWorld.spec.js
tobi-fis commented 5 months ago

Any news on this? Some component logic I need to test relies on useSlots and I still get Cannot read properties of null (reading 'setupContext') when running the test.

Update for anyone struggling with this: The solution was to put the call to useSlots() into the top-level of your components' script setup block (or setup function). I had placed it inside a function before, heres the before/after example:

Wrong:

<script setup>
  const foo = () => {
     const accordionItems = useSlots().accordions?.();  
     # other logic...
  }
</script>

Correct:

<script setup>
  const accordionItems = useSlots().accordions?.();

  const foo = () => {
   # other logic...
  }
</script>

Currently using: