Skip to content

fix: keep the namespace prefix in attributes() - #2941

Merged
cexbrayat merged 1 commit into
vuejs:mainfrom
MaxFreedomPollard:fix/namespaced-attributes
Sep 18, 2026
Merged

cexbrayat merged 1 commit into
vuejs:mainfrom
MaxFreedomPollard:fix/namespaced-attributes

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Summary

attributes() indexed its result by the attribute's local name, so a namespaced attribute lost its prefix. xlink:* attributes on SVG elements were reported under the wrong key, and an element carrying both xlink:href and href silently lost one of the two.

Root cause

src/baseWrapper.ts line 286 built the map with attributeMap[attribute.localName] = attribute.value. Attr.localName is the name with the namespace prefix removed, so xlink:href is stored as href. Vue sets xlink:* attributes on SVG elements through setAttributeNS (patchAttr in @vue/runtime-dom), which is the ordinary path for SVG sprite icons, so those attributes always arrive namespaced.

Mounting <svg><use xlink:href="#icon" href="#fallback" /></svg> on main gives:

wrapper.find('use').html() // '<use xlink:href="#icon" href="#fallback"></use>'
wrapper.find('use').attributes() // { href: '#fallback' }, xlink:href is gone
wrapper.find('use').attributes('xlink:href') // undefined

Both attributes collide on the key href and the last one wins, so attributes() and html() disagree about what the element carries.

The same element shows the inconsistency from the other side: xmlns:xlink is written with plain setAttribute and therefore has no namespace, so its prefix survives, while xlink:href loses its own.

Fix

Index by Attr.name, the qualified name, which is exactly what html() renders.

HTML attribute names are lowercased by the parser, so name and localName are the same string for every attribute without a prefix, and unprefixed attributes are untouched. On an element that carries only xlink:href, attributes('href') no longer resolves to it: it is now reported under xlink:href.

Validation

The regression test added to tests/attributes.spec.ts fails on main with AssertionError: expected { href: '#fallback' } to deeply equal { 'xlink:href': '#icon', ... } and passes with the fix.

  • vitest run --maxWorkers=3: 54 files, 505 passed, 1 todo, 0 failed
  • rollup -c rollup.config.ts --bundleConfigAsCjs then vitest run --mode test-build tests/attributes.spec.ts: 6 passed, confirming the built bundle behaves the same
  • pnpm run lint: exit 0, and oxlint src/baseWrapper.ts tests/attributes.spec.ts reports nothing
  • pnpm run format-check: all 239 files correctly formatted
  • pnpm run tsd: exit 0
  • pnpm run vue-tsc: exit 0

`attributes()` indexed the returned map by `Attr.localName`, which strips
the namespace prefix. Vue sets `xlink:*` attributes on SVG elements with
`setAttributeNS`, so `<use xlink:href="#icon" />` was reported as
`{ href: '#icon' }`, `attributes('xlink:href')` returned `undefined`, and
an element carrying both `xlink:href` and `href` lost one of them because
the two collided on the same key.

Index by `Attr.name` instead, which is the qualified name and matches what
`html()` renders. HTML attribute names are already lowercased by the
parser, so unprefixed attributes are unaffected.
@netlify

netlify Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vue-test-utils-docs ready!

Name Link
🔨 Latest commit cc7def5
🔍 Latest deploy log https://app.netlify.com/projects/vue-test-utils-docs/deploys/6a9cdcb6882a4f000827eb27
😎 Deploy Preview https://deploy-preview-2941--vue-test-utils-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@simonyang08

Copy link
Copy Markdown
Contributor

Reviewed the change and verified the DOM semantics it relies on (jsdom, same as the test env): for <use xlink:href="#icon" href="#fallback" viewBox="0 0 2 2">, the two keying strategies produce

// main (attribute.localName) — xlink value silently lost to the collision
{ href: '#fallback', viewBox: '0 0 2 2' }
// PR (attribute.name) — both preserved
{ 'xlink:href': '#icon', href: '#fallback', viewBox: '0 0 2 2' }

Two things worth noting in favor:

  • For every non-prefixed attribute — including case-adjusted SVG ones like viewBox — Attr.name === Attr.localName, so this is a strictly additive fix; nothing that works today changes.
  • Vue sets xlink:* through setAttributeNS (as the PR body says), so the reproduction is realistic, and wrapper.attributes('xlink:href') becomes usable.

No other concerns: the regression test locks both the prefixed key and the coexistence case. One process note — the Node build checks haven't run on this PR yet (only the Netlify ones), which looks like the first-contribution workflow-approval gate rather than anything about the change; worth a maintainer click.

From my side this is good to merge once CI has run.

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vue/test-utils@2941

commit: cc7def5

@cexbrayat cexbrayat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

@cexbrayat
cexbrayat merged commit ade8187 into vuejs:main Sep 18, 2026
9 checks passed
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.

3 participants