Skip to content

fix(runtime-utils): provide NuxtLink isActive slot props#1640

Open
Archetipo95 wants to merge 4 commits intonuxt:mainfrom
Archetipo95:fix/router-link-is-active-slot-props
Open

fix(runtime-utils): provide NuxtLink isActive slot props#1640
Archetipo95 wants to merge 4 commits intonuxt:mainfrom
Archetipo95:fix/router-link-is-active-slot-props

Conversation

@Archetipo95
Copy link
Copy Markdown

Linked issue

Closes #1042

Description

This fixes NuxtLink custom slot behavior in mountSuspended by providing isActive and isExactActive slot props from the RouterLink test stub.

  • update RouterLink stub to expose isActive and isExactActive in custom-slot mode
  • add mountSuspended tests for active and inactive route states
  • add a fixture component reproducing v-slot="{ isActive }" usage

Validation

  • VITEST_DOM_ENV=happy-dom pnpm test:unit --run
  • VITEST_DOM_ENV=jsdom pnpm test:unit --run

@Archetipo95 Archetipo95 requested a review from danielroe as a code owner March 30, 2026 07:08
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

Open in StackBlitz

npm i https://pkg.pr.new/nuxt/test-utils/@nuxt/test-utils@1640
npm i https://pkg.pr.new/nuxt/test-utils/vitest-environment-nuxt@1640

commit: f8631eb

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1bd38046-96a4-44a0-bc9f-0f11a474ae89

📥 Commits

Reviewing files that changed from the base of the PR and between a0e458a and f8631eb.

📒 Files selected for processing (5)
  • examples/app-vitest-full/components/NuxtLinkWithIsActive.vue
  • examples/app-vitest-full/pages/about.vue
  • examples/app-vitest-full/pages/about/team.vue
  • examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
  • src/runtime-utils/components/RouterLink.ts
✅ Files skipped from review due to trivial changes (3)
  • examples/app-vitest-full/pages/about/team.vue
  • examples/app-vitest-full/pages/about.vue
  • examples/app-vitest-full/components/NuxtLinkWithIsActive.vue
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/runtime-utils/components/RouterLink.ts
  • examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts

📝 Walkthrough

Walkthrough

The changes add a new example Vue component (NuxtLinkWithIsActive.vue) and two example pages (/about and /about/team). Tests were added to assert NuxtLink custom-slot active-state behavior using mountSuspended. The RouterLink runtime was modified to use useLink(props) (instead of useRouter()), derive route, href, isActive, and isExactActive from the returned link, and to pass isActive and isExactActive into the custom slot; the non-custom anchor now uses link.href and calls link.navigate on click.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: fixing NuxtLink to provide isActive slot props in the RouterLink test stub.
Description check ✅ Passed The description is related to the changeset and explains what was fixed, how it was fixed, and how to validate the changes.
Linked Issues check ✅ Passed The PR fully addresses issue #1042 by updating RouterLink stub to expose isActive/isExactActive slot props, adding tests for active/inactive states, and including a fixture component demonstrating v-slot usage.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing NuxtLink custom slot behavior and testing it; new pages (about.vue, about/team.vue) are only fixture files supporting the tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts (1)

551-552: Prefer deterministic link selection in assertions.

Using text-search over all anchors is a bit brittle. Selecting the About anchor by href makes these tests more stable.

Suggested refinement
-  const aboutLink = component.findAll('a').find(el => el.text().includes('About'))
-  expect(aboutLink?.classes()).toContain('active')
+  const aboutLink = component.get('a[href="/about"]')
+  expect(aboutLink.classes()).toContain('active')
...
-  const aboutLink = component.findAll('a').find(el => el.text().includes('About'))
-  expect(aboutLink?.classes()).not.toContain('active')
+  const aboutLink = component.get('a[href="/about"]')
+  expect(aboutLink.classes()).not.toContain('active')

Also applies to: 560-561

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts` around lines 551
- 552, Replace the brittle text-based anchor lookup (aboutLink =
component.findAll('a').find(el => el.text().includes('About'))) with a
deterministic selector that targets the About link by its href attribute (use
component.find or component.findAll with 'a[href="/about"]'), then assert its
classes() contains 'active'; update the same pattern at the other occurrence
that currently uses text-search to ensure stable tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime-utils/components/RouterLink.ts`:
- Around line 25-26: The two flags are incorrectly identical; change the
assignment so isActive checks whether the link's route record appears anywhere
in the currentRoute.matched hierarchy (e.g., use
currentRoute.matched.some(record => record === route || record.name ===
route.name)), while isExactActive remains the exact route match (e.g., compare
route.path or route.name to currentRoute.path/name). Update the variables
isActive and isExactActive in the RouterLink component to use these distinct
checks referencing route and currentRoute.

---

Nitpick comments:
In `@examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts`:
- Around line 551-552: Replace the brittle text-based anchor lookup (aboutLink =
component.findAll('a').find(el => el.text().includes('About'))) with a
deterministic selector that targets the About link by its href attribute (use
component.find or component.findAll with 'a[href="/about"]'), then assert its
classes() contains 'active'; update the same pattern at the other occurrence
that currently uses text-search to ensure stable tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0ca9886-c1bf-4aa5-85da-124731e6fd2c

📥 Commits

Reviewing files that changed from the base of the PR and between 3218b87 and a0e458a.

📒 Files selected for processing (3)
  • examples/app-vitest-full/components/NuxtLinkWithIsActive.vue
  • examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
  • src/runtime-utils/components/RouterLink.ts

Archetipo95 and others added 2 commits March 30, 2026 11:48
use vue-router useLink in RouterLink mock

track nested-route NuxtLink-in-suspended limitation via expected-fail regression test
@Archetipo95
Copy link
Copy Markdown
Author

Im not sure if I can tag @coderabbitai, but the latest commit has the fix for isActive/isExactActive issue by changing the RouterLink stub to use useLink from vue-router instead of manual path matching.

One known limitation came up in testing. When NuxtLink is used with a custom slot inside mountSuspended, nested routes do not keep the parent link active. For example, /about is not marked active when the current route is /about/team.

This happens because NuxtLink looks for a .useLink property on the resolved RouterLink component to compute active state. Custom static properties are removed from components registered through global.components, so NuxtLink falls back to exact path comparison. So the exact match case still works, but the nested route case does not.I left that as an expected failing test for now: it.fails('keeps parent link active without exact match on nested routes...)

A proper fix would likely need to happen in VTU or in mountSuspended @danielroe do you agree or it's too overengineered?

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the RouterLink stub implementation
cat src/runtime-utils/components/RouterLink.ts

# Check NuxtLink source or how useLink property is checked
rg -n "useLink" src/ --type ts -C3

# Check how global.components registration works in mountSuspended
rg -n "global.components\|RouterLink" src/ --type ts -C3

Length of output: 1848


🏁 Script executed:

#!/bin/bash
# Check how NuxtLink uses the useLink property - look in node_modules or any nuxt source
rg -rn "useLink" --type ts -C3 -g "!src/"

# Check the mountSuspended implementation for how RouterLink is registered
rg -rn "RouterLink\|global.*component" src/ --type ts -C5

# Check VTU's handling of component stubs and static properties
rg -rn "useLink\|RouterLink" packages/ --type ts -C3 2>/dev/null || echo "No packages dir"

# Look at the full mount-suspended implementation
fd "mount-suspended" --type f --exec cat {}

Length of output: 36534


@Archetipo95 Yes, you can absolutely tag me! Thanks for the detailed writeup — the approach and the tradeoff are both well-reasoned. Here's my analysis:

On the primary fix: Using useLink(props) is the right call. It delegates isActive/isExactActive computation to vue-router's own logic rather than reimplementing it, and the two new passing tests confirm the exact-match cases work correctly now.

On the nested route limitation: The root cause you've identified is accurate. NuxtLink checks for a .useLink static property on the resolved RouterLink component (to use vue-router's composable directly for non-exact active-state computation). Because VTU strips static properties from components registered via global.components, NuxtLink can't find it and falls back to exact path comparison.

A few potential avenues worth considering:

  1. Attach useLink to the stub before VTU registration — in mountSuspended, you could try manually assigning RouterLink.useLink = useLink to the stub object after it's been placed into global.components, by reaching into the VTU app's component registry. This is fragile and couples mountSuspended to VTU internals, so probably not ideal.

  2. Use VTU stubs instead of global.components — registering via stubs may preserve the component object reference differently. Worth a quick spike to see if static properties survive that path.

  3. Upstream VTU issue — the cleanest long-term fix would be for VTU to preserve static properties on globally registered components. This would be broadly useful beyond just this case.

  4. Upstream Nuxt issue — alternatively, Nuxt could make NuxtLink's useLink detection more test-environment-friendly (e.g., falling back to useLink directly when the property isn't found on the stub).

Given the complexity, the it.fails regression test is the right call for now — it clearly documents the boundary of what's currently supported without blocking the main fix for #1042. Whether a deeper fix belongs in VTU or mountSuspended is worth a discussion with @danielroe, and I agree it leans toward VTU as the more principled layer.

(´• ω •`) 🐇


✏️ Learnings added
Learnt from: Archetipo95
URL: https://github.com/nuxt/test-utils/pull/1640

Timestamp: 2026-03-30T10:17:20.958Z
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeError: Cannot destructure property 'isActive' of 'undefined' as it is undefined - NuxtLink

1 participant