Test fails if useNuxtApp used
Environment
- Operating System: Linux
- Node Version: v18.19.0
- Nuxt Version: 3.9.3
- CLI Version: 3.10.0
- Nitro Version: 2.8.1
- Package Manager: [email protected]
- Builder: -
- Build Modules: -
Reproduction
https://stackblitz.com/edit/github-vy6psx-sn3lfr
Describe the bug
[Vue warn]: Cannot mutate <script setup> binding "nuxtApp" from Options API. at <MountSuspendedComponent > at <MountSuspendedHelper> at <Anonymous ref="VTU_COMPONENT" > at <VTUROOT> [Vue warn]: Unhandled error during execution of render function at <MountSuspendedComponent > at <MountSuspendedHelper> at <Anonymous ref="VTU_COMPONENT" > at <VTUROOT>
I get this error message whenever I try to use useNudxtApp() in a component. Before the merge of nuxt-vitest and @nuxt/text-utils, this worked fine.
Additional context
Tests can be ran with: npm run test ./pages/tests/index.spec.ts
Logs
No response
I have the same issue. If I rename nuxtApp to anything else, it works: const _nuxtApp = useNuxtApp(). But that is not ideal, I would have to rename it to something weird in hundreds of places in our app.
Edit: I found a very similar issue: https://github.com/frontegg/frontegg-vue/issues/348 And that is not even related to Nuxt. I have no clue why the renaming solves it.
I'm seeing this same issue in my most basic test:
<!-- File: /layouts/mount-suspended.vue -->
<template>
<div>
<div>
<h1>{{ isHydrated }}</h1>
<NuxtPage />
</div>
</div>
</template>
<script setup>
import { useNuxtApp } from '#app'
const nuxtApp = useNuxtApp()
const isHydrated = ref(nuxtApp.isHydrating)
</script>
And in my Vitest test:
// File: /layouts/tests/mount-suspended.test.js
// @vitest-environment nuxt
import { mountSuspended } from '@nuxt/test-utils/runtime'
import layoutToMountSuspended from '@/layouts/mount-suspended'
describe('mountSuspended experiment', () => {
let wrapper
beforeEach(async () => {
wrapper = await mountSuspended(layoutToMountSuspended, {
global: {
stubs: ['NuxtPage'],
},
})
})
it('is a component', () => {
expect(wrapper.vm).toBeTruthy()
})
})
This test fails with the same error as above, but for me I have to not only rename the constant to something other than nuxtApp, but I also have to alias the useNuxtApp import itself:
import { useNuxtApp as whatever } from '#app'
const bobLoblaw = whatever()
const isHydrated = ref(bobLoblaw.isHydrating)
Now my test runs successfully. I'm guessing there's some sort of auto-import issue that scans files for those exact names. (I'd like to add that I've also seen issues in the past, even if an automatic Nuxt import appears only in a commented section. It still reads the comments and causes issues. So watch out for commented out code too!
Thanks @kabalage. This has been one of the most perplexing and difficult-to-solve issues I've come across in a long time.
EDIT: All that said, I hope this issue will still be addressed. Changing the alias/name throughout all of our files doesn't seem like a very tenable solution, nor is this issue documented anywhere except here.
I'd like to add that I'm seeing the same error for other imports as well, such as useRoute or useRouter from vue-router when using mountSuspended():
[Vue warn]: Cannot mutate <script setup> binding "useRouter" from Options API.
I don't know how mountSuspended works under the hood but it seems to be using the Options API and is attempting to mutate certain imports unless they get aliased to a different name. This is highly problematic.
Update: Sometimes the issue is that the test simply times out after 10 seconds, and renaming useRouter to aliasedUseRouter fixes it.
I've been researching this problem some more, and I found another workaround that doesn't require aliasing useNuxtApp (or useRoute or useRouter). The other "solution" is to just remove the import statements entirely and rely on them as auto-imports. If I do that I can get my most basic tests to pass, without having to rename/alias every import.
However, this is also problematic because we've been using
import { useRoute, useRouter } from 'vue-router'
throughout our app, and when we remove this and instead rely on Nuxt's auto-import feature, the auto-import for the route/router is coming from Nuxt instead of vue-router, and that means you have to mock it with mockNuxtImport() instead of vi.mock(). That would mean updating a TON of tests on our end, so this solution isn't ideal either.
Nevertheless I'm posting this here since people are going to need every workaround they can for this issue until it is addressed.
So I tried debugging the issue and I made some progress on this.
To debug I ran vitest with --inspect-brk --no-file-parallelism and used chrome://inspect to attach to the process.
I looked for the warning thrown by vue: "Cannot mutate <script setup> binding ..."
I found it in runtime-core.cjs, set a breakpoint for the line, and let the debugger resume. (Source: https://github.com/vuejs/core/blob/v3.5.12/packages/runtime-core/src/componentPublicInstance.ts#L546)
Inspecting the runtime objects, I figured out that anything defined in the setup of the Nuxt root component cannot be overriden.
So basically none of these variable names work in script setup components when mounting via mountSuspended:
[
"IslandRenderer",
"nuxtApp",
"onResolve",
"url",
"SingleRenderer",
"results",
"error",
"abortRender",
"islandContext",
"defineAsyncComponent",
"onErrorCaptured",
"onServerPrefetch",
"provide",
"useNuxtApp",
"isNuxtError",
"showError",
"useError",
"useRoute",
"useRouter",
"PageRouteSymbol",
"AppComponent",
"ErrorComponent",
"componentIslands"
]
From inspecting the callstack, this where the warning happens: https://github.com/nuxt/test-utils/blob/v3.14.4/src/runtime-utils/mount.ts#L128
I'm not exactly sure how this cloning/proxying is done in mountSuspended, but my guess is we should not merge the setupState of the root component with the setup state of the component being mounted. Or prefix the variables in nuxt-root.vue so it's not causing collisions.
Sorry to ping you @danielroe, I know you must be getting many notifications, but I think this needs some of your attention. What direction should I take here? I tried the latter locally, that works, but nuxt-root.vue becomes quite ugly and it's a test-utils concern anyways.
(#871 and #986 are both the same issue as this.)