Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/runtime/shared/vue-wrapper-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { config } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'

type Options = ReturnType<typeof createPluginOptions>
interface Options {
hasNuxtPage(): boolean
}

const PLUGIN_NAME = 'nuxt-test-utils'

// @vue/test-utils is optional
// If import fails, hasNuxtPage always returns false
const config = await import('@vue/test-utils')
.then(r => r.config)
.catch(() => undefined)

export function getVueWrapperPlugin(): Options {
if (!config) {
return {
hasNuxtPage: () => false,
}
}
Comment on lines +11 to +20
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the vue-wrapper-plugin.ts file to see the current state
cat -n src/runtime/shared/vue-wrapper-plugin.ts

Repository: nuxt/test-utils

Length of output: 2045


🏁 Script executed:

# Now check how getVueWrapperPlugin() is consumed in nuxt.ts
cat -n src/runtime/shared/nuxt.ts | head -30

Repository: nuxt/test-utils

Length of output: 938


🏁 Script executed:

# Check if there are any other usages of getVueWrapperPlugin in the codebase
rg "getVueWrapperPlugin" --type ts --type tsx -n

Repository: nuxt/test-utils

Length of output: 86


🏁 Script executed:

# Retry search for getVueWrapperPlugin with simpler approach
rg "getVueWrapperPlugin" -n

Repository: nuxt/test-utils

Length of output: 531


🏁 Script executed:

# Check package.json to see if vue/test-utils is optional or required
cat package.json | grep -A 5 -B 5 "test-utils"

Repository: nuxt/test-utils

Length of output: 2056


🏁 Script executed:

# Check Node.js version and what error codes are typical for module not found
# Also verify how top-level await works with module initialization
python3 << 'EOF'
import json

# Document error code behavior for dynamic import failures
error_behaviors = {
    "ERR_MODULE_NOT_FOUND": "Module does not exist in filesystem/registry",
    "ERR_UNKNOWN_FILE_EXTENSION": "File has unrecognized extension",
    "SyntaxError": "Module has syntax error",
    "ReferenceError": "Module has undefined reference",
    "TypeError": "Module execution throws TypeError",
}

print("Dynamic import failure scenarios:")
for code, desc in error_behaviors.items():
    print(f"  {code}: {desc}")

print("\nTop-level await behavior:")
print("  - Module with top-level await becomes 'async module'")
print("  - Importers of that module wait for initialization to complete")
print("  - But functions IN that module remain synchronous")
EOF

Repository: nuxt/test-utils

Length of output: 557


🏁 Script executed:

# Look at the full package.json structure
head -40 package.json

Repository: nuxt/test-utils

Length of output: 1019


🏁 Script executed:

# Verify standard Node.js error codes for module not found
python3 << 'EOF'
# Node.js module resolution error patterns
print("Node.js Module Resolution Errors:")
print()
print("When require/import fails to resolve a module:")
print("  - Error.code === 'ERR_MODULE_NOT_FOUND' (most common)")
print("  - Error.message typically includes: 'Cannot find module ...'")
print("  - Error.message typically includes: 'no such file or directory'")
print()
print("When a module exists but fails during evaluation:")
print("  - Error.code may be undefined")
print("  - Error could be SyntaxError, ReferenceError, TypeError, etc.")
print("  - These are real bugs that should NOT be silently swallowed")
print()
print("Key insight for `@vue/test-utils`:")
print("  - If package is genuinely optional, catch only resolution failures")
print("  - If package exists but is broken, that's a configuration bug")
print("  - Silently returning hasNuxtPage: () => false hides the real problem")
EOF

Repository: nuxt/test-utils

Length of output: 772


🏁 Script executed:

# Check if there's any existing error handling pattern in the codebase
rg "ERR_MODULE_NOT_FOUND|error\.code" --type ts -A 3 -B 1 | head -50

Repository: nuxt/test-utils

Length of output: 41


🏁 Script executed:

# Verify the exact structure of r.config to confirm the .then(r => r.config) is correct
# Look for any type definitions or usage
rg "@vue/test-utils" src/ -A 2 -B 2 --type ts | head -40

Repository: nuxt/test-utils

Length of output: 1741


Distinguish between missing and broken @vue/test-utils.

The blanket .catch(() => undefined) treats all import failures identically—missing packages, broken installations, unsupported export shapes, and evaluation errors all silently become hasNuxtPage: () => false. This hides real configuration bugs. Since @vue/test-utils is optional, catch only module resolution failures and rethrow everything else.

Recommended approach
-const config = await import('@vue/test-utils')
-  .then(r => r.config)
-  .catch(() => undefined)
+const config = await import('@vue/test-utils')
+  .then((r) => {
+    if (!r.config) {
+      throw new Error('Unsupported `@vue/test-utils` version: missing config export')
+    }
+    return r.config
+  })
+  .catch((error: any) => {
+    const isMissingVueTestUtils =
+      error?.code === 'ERR_MODULE_NOT_FOUND'
+      && String(error.message).includes('@vue/test-utils')
+    if (isMissingVueTestUtils) return undefined
+    throw error
+  })

Adjust the error predicate as needed for your target module systems, but the key point is to avoid swallowing non-resolution failures.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const config = await import('@vue/test-utils')
.then(r => r.config)
.catch(() => undefined)
export function getVueWrapperPlugin(): Options {
if (!config) {
return {
hasNuxtPage: () => false,
}
}
const config = await import('@vue/test-utils')
.then((r) => {
if (!r.config) {
throw new Error('Unsupported `@vue/test-utils` version: missing config export')
}
return r.config
})
.catch((error: any) => {
const isMissingVueTestUtils =
error?.code === 'ERR_MODULE_NOT_FOUND'
&& String(error.message).includes('@vue/test-utils')
if (isMissingVueTestUtils) return undefined
throw error
})
export function getVueWrapperPlugin(): Options {
if (!config) {
return {
hasNuxtPage: () => false,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/shared/vue-wrapper-plugin.ts` around lines 9 - 18, The current
import of '@vue/test-utils' swallows all errors and hides real issues; change
the dynamic import logic used to set config (the import('@vue/test-utils') =>
r.config pattern) so the catch only converts "module not found" resolution
errors into undefined and rethrows any other errors; in practice replace the
blanket .catch(() => undefined) with logic that inspects the thrown error (e.g.
error.code === 'MODULE_NOT_FOUND' or 'ERR_MODULE_NOT_FOUND' or message patterns
for missing module) and returns undefined only for those cases, otherwise
rethrow so getVueWrapperPlugin and its hasNuxtPage behavior aren't masking
broken installations or runtime failures.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasNuxtPage is only meaningful when @vue/test-utils is available and used in tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yamachi4416, that's a fair point — since hasNuxtPage() ultimately relies on VueWrapper instances that only exist when @vue/test-utils is installed and actively used in tests, returning false is always the correct semantic fallback regardless of the reason the import failed. I'll withdraw my suggestion.


🧠 Learnings used
Learnt from: Archetipo95
Repo: nuxt/test-utils PR: 0
File: :0-0
Timestamp: 2026-03-30T10:17:20.984Z
Learning: In nuxt/test-utils, the `RouterLink` stub in `src/runtime-utils/components/RouterLink.ts` uses `useLink(props)` from vue-router to derive `isActive`, `isExactActive`, `href`, `route`, and `navigate`. The custom slot provides all five. Nested-route parent-active state does not work in `mountSuspended` because VTU strips static properties (including `.useLink`) from components registered via `global.components`, so NuxtLink falls back to exact path comparison. This is tracked as a known limitation with an `it.fails` regression test.


const installed = config.plugins.VueWrapper.installedPlugins
.find(({ options }) => options?._name === PLUGIN_NAME)

Expand Down
Loading