Skip to content

Component props are passed as ref instead of refs value when using defineComponent() #1199

@AndreasMietk

Description

@AndreasMietk

Environment

  • Operating System: Linux
  • Node Version: v20.18.1
  • Nuxt Version: 3.15.4
  • CLI Version: 3.21.1
  • Nitro Version: 2.10.4
  • Package Manager: bun@1.2.5
  • Builder: -
  • User Config: $production, compatibilityDate, telemetry, devtools, ssr, imports, runtimeConfig, vite, build, modules, css, app, auth, sourcemap
  • Runtime Modules: @nuxt/test-utils/module@3.17.2, ~/modules/vuetify.ts, ~/modules/DynamicCSSStyles.ts, nuxt-svgo@4.0.13, @sidebase/nuxt-auth@0.10.0, @nuxt/eslint@0.7.5, @sentry/nuxt/module@8.52.0
  • Build Modules: -

Reproduction

component

<template>
  <v-menu v-model="menuShow">
    <template #activator="{ props }">
      <v-btn v-bind="props" />
    </template>
    <v-list v-model:selected="selected" selectable density="default" max-height="40vh" select-strategy="leaf">
      <template v-for="item in items" :key="item.title">
        <v-list-item>
          {{ item.title }}
        </v-list-item>
      </template>
    </v-list>
  </v-menu>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'

export default defineComponent({
  name: 'AppTestComponent',
  setup() {
    const selected = ref([])
    const menuShow = ref(false)
    return {
      menuShow,
      selected,
    }
  },
  computed: {
    items() {
      return [
        {
          title: 'My Title',
        },
      ]
    },
  },
})
</script>

test

import { describe, expect, test } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import AppTestComponent from '~/components/AppTestComponent.vue'
import { mount } from '@vue/test-utils'
import { VList } from 'vuetify/components/VList'

describe('AppTestComponent', () => {
  test('renders', async () => {
    const component = mount(AppTestComponent)

    await component.find('button').trigger('click')
    const listContent = component.findComponent(VList).html()
    expect(listContent).toContain('My Title')
  })
  test('will fail', async () => {
    const component = await mountSuspended(AppTestComponent)

    await component.find('button').trigger('click')
    const listContent = component.findComponent(VList).html()
    expect(listContent).toContain('My Title')
  })
})

Describe the bug

When testing Vuetify components with mountSuspended from @nuxt/test-utils/runtime, I discovered that ref values are not properly unwrapped before being passed to components.

Specifically, when mounting Vuetify's VMenu or VList components, props like modelValue (for VMenu) or selected (for VList) receive the actual ref object instead of the ref's value. This behavior differs from Vue Test Utils' standard mount function, which correctly unwraps ref values.

Reproduction Steps

The issue occurs when:

  1. Using mountSuspended from @nuxt/test-utils/runtime
  2. Testing components created with defineComponent()
  3. Passing refs as props to Vuetify components from setup()

Interestingly, components using <script setup> syntax work correctly.

Error Details

The error occurs in Vuetify's internal code when it tries to iterate over what it expects to be an array value but receives a ref object instead:

TypeError: (v || []) is not iterable
 ❯ Object.in node_modules/vuetify/src/composables/nested/selectStrategies.ts:56:25
 ❯ transformIn node_modules/vuetify/src/composables/nested/nested.ts:184:33
 ❯ ComputedRefImpl.get [as fn] node_modules/vuetify/src/composables/proxiedModel.ts:53:14
 ❯ refreshComputed node_modules/@vue/reactivity/dist/reactivity.cjs.js:384:28
 ❯ ComputedRefImpl.value node_modules/@vue/reactivity/dist/reactivity.cjs.js:1625:5
 ❯ ComputedRefImpl.fn node_modules/vuetify/src/composables/nested/nested.ts:356:53
 ❯ refreshComputed node_modules/@vue/reactivity/dist/reactivity.cjs.js:384:28
 ❯ ComputedRefImpl.value node_modules/@vue/reactivity/dist/reactivity.cjs.js:1625:5
 ❯ ComputedRefImpl.fn node_modules/vuetify/src/components/VList/VListItem.tsx:141:105
 ❯ refreshComputed node_modules/@vue/reactivity/dist/reactivity.cjs.js:384:28

Investigation Findings

When debugging the Vuetify components by placing breakpoints in their setup functions, I observed that:

  • With @vue/test-utils mount: Props contain the actual values of refs
  • With @nuxt/test-utils mountSuspended: Props contain the ref objects themselves

I suspect this is related to how mountSuspended handles the return value of setup() when using defineComponent(), failing to properly unwrap reactive references before passing them as props.

Additional context

No response

Logs

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions