Vue Test Utils stubs <Transition> by default but not <Teleport>; what does each default mean for what a test can see and assert?
answer
- two built-ins, two defaults
- no waiting for animations
- hooks the stub never calls
- content moved outside the wrapper
- opt in with a lowercase key
basics
~20 sTransition and TransitionGroup are stubbed by default: toggled content appears or vanishes on the next render with no animation, but transition JavaScript hooks never run. Teleport stays real, so its content leaves wrapper.find()'s reach unless you stub teleport: true.
solid answer
~50 sVue Test Utils' default config is `global.stubs = { transition: true, 'transition-group': true }`. The stubs render `<transition-stub>` around the slot content, so a `v-if` inside `<Transition>` shows or disappears as soon as the component re-renders — no waiting for a leave animation. The catch: the stub does not support transition JavaScript hooks, so logic in `@after-leave` or `@enter` never runs; set `transition: false` to use the real component when that logic matters. `<Teleport>` is not stubbed by default: its content really moves to the target, so the target must exist (otherwise Vue warns `Failed to locate Teleport target with selector`), and `wrapper.find()` or `html()` show only teleport comment anchors. Either reach the teleported child with `findComponent()`, which walks the virtual tree, or add `teleport: true` to `global.stubs`, which renders the content inline inside `<teleport-stub>` so `find()` works.
code
ts · 27 linesimport { mount } from '@vue/test-utils'
import Navbar from './Navbar.vue'
import Signup from './Signup.vue'
beforeEach(() => {
const el = document.createElement('div')
el.id = 'modal'
document.body.appendChild(el)
})
afterEach(() => {
document.body.innerHTML = ''
})
test('real Teleport: reach the moved child through the component tree', async () => {
const wrapper = mount(Navbar)
expect(wrapper.find('input').exists()).toBe(false)
const signup = wrapper.getComponent(Signup)
await signup.get('input').setValue('valid_username')
await signup.get('form').trigger('submit')
expect(signup.emitted('signup')?.[0]).toEqual(['valid_username'])
})
test('stubbed Teleport: content renders inline', () => {
const wrapper = mount(Navbar, { global: { stubs: { teleport: true } } })
expect(wrapper.find('input').exists()).toBe(true)
})go deeper
Remember the defaults: transition and transition-group are stubbed, teleport is not. Teleported content is outside wrapper.find()'s reach.
Explain why the transition stub removes timing from tests but skips JavaScript hooks, and the two ways to reach teleported content: findComponent on the virtual tree, or teleport: true to render it inline.
Choose per test what the stub may hide. Turn the transition stub off where hook logic such as focus return matters, and keep the real Teleport where the destination itself is part of the contract.
Agree suite-wide defaults for built-in stubs in the shared test setup and document which behaviours then need dedicated tests, so hook and teleport-target regressions have a named owner.
## Two built-ins, two different defaults Vue 3 ships built-in components that change **where or when** content appears: `<Transition>` and `<TransitionGroup>` delay entering and leaving content so CSS or JavaScript can animate it, and `<Teleport>` renders its content into another part of the document. **Vue Test Utils** (`@vue/test-utils` 2.x) treats them differently out of the box. Its global configuration starts as: ```ts config.global.stubs = { transition: true, 'transition-group': true } ``` So under a normal `mount`, the two transition components are **stubbed unless you opt out**, and `<Teleport>` (like `<KeepAlive>`) is **real unless you opt in**. | built-in | default under a full mount | opt the other way | |---|---|---| | `<Transition>` | stubbed as `<transition-stub>` | `global: { stubs: { transition: false } }` | | `<TransitionGroup>` | stubbed as `<transition-group-stub>` | `global: { stubs: { 'transition-group': false } }` | | `<Teleport>` | real | `global: { stubs: { teleport: true } }` | ## What the transition stub gives you A real `<Transition>` keeps a leaving element in the DOM until its leave transition finishes and applies timed classes such as `v-enter-from` and `v-leave-active`. In a simulated DOM that timing is noise: a test that clicks "close" wants to assert the dialog is gone. The built-in stub renders its **default slot directly** inside a `<transition-stub>` element. Consequences: - content toggled with `v-if` appears or disappears on the next re-render — the same re-render any other state change needs; - no enter/leave classes are applied and no animation timing is involved; - `wrapper.html()` contains `<transition-stub ...>` wrappers, which matters if you snapshot markup. The documented limitation is that the stub does **not support transition JavaScript hooks**. If a component moves focus back to a trigger button in `@after-leave`, or emits `closed` from it, that code never runs under the default stub, and the test cannot see it. Options: 1. set `transition: false` for that test so the real `<Transition>` runs — the guide notes you may then also need to make `requestAnimationFrame` fire immediately through your runner's mocking API; 2. provide your own transition stub that calls the hooks you depend on; 3. test the hook's logic somewhere it does not depend on the transition. ## What a real Teleport means for a test Take a `Navbar` whose template contains `<Teleport to="#modal"><Signup /></Teleport>`: - the target must exist in the document before mounting; otherwise Vue warns `Failed to locate Teleport target with selector "#modal"`, so tests usually create it in a setup hook and remove it afterwards; - the teleported markup lives inside that target, not inside the wrapper's element, so `wrapper.html()` shows only `<!--teleport start-->` and `<!--teleport end-->` in development builds, and `wrapper.find('input')` finds nothing; - the component tree is unchanged, so `wrapper.findComponent(Signup)` or `getComponent(Signup)` still returns a `VueWrapper`, from which `find()`, `trigger()` and `emitted()` work on the teleported content. ## Stubbing Teleport With `global: { stubs: { teleport: true } }`, the built-in stub renders the teleport's content **in place**, inside `<teleport-stub to="#modal">`. `wrapper.find()` now sees it, and no target element is needed. Keys for these built-ins are the lowercase names (`teleport`, `transition`, `transition-group`); PascalCase spellings are accepted too. ## Choosing, and what each choice hides - **Default transition stub** — right for most tests that assert end states. It hides hook-driven behaviour and anything that depends on the leaving element staying in the DOM during its animation. - **Real Teleport plus `findComponent`** — keeps the real move, so a wrong `to` selector or a missing target shows up as a warning in the test output. - **`teleport: true`** — simplest assertions, but it would not notice that a modal is teleported to the wrong place or not teleported at all. Pick the default that matches what the test claims to prove, and when a stub is chosen, state what it no longer checks. ## Where to set these defaults The defaults live on the shared `config` object exported by `@vue/test-utils`, so they can be changed once for the whole suite in the runner's setup file, or per test through the `global.stubs` mounting option: - **suite-wide**: `config.global.stubs.transition = false` turns the real `<Transition>` on everywhere — useful when many components rely on transition hooks; - **per test**: `mount(Comp, { global: { stubs: { teleport: true } } })` affects only that mount, which keeps the exception visible where it is used; - **mixing both**: a per-mount entry overrides the suite-wide entry with the same key, so a test can re-enable a real built-in the setup file stubbed. Keeping per-test overrides next to the test that needs them makes a reviewer see immediately which behaviour that test has switched off.
- A dialog emits `closed` from its Transition's `@after-leave` hook, and a Vue Test Utils test waits for that event in vain; what is going on?The default transition stub does not support transition JavaScript hooks, so `@after-leave` never fires and `closed` is never emitted. Mount that test with `global: { stubs: { transition: false } }` so the real `<Transition>` runs, making `requestAnimationFrame` fire immediately if needed, or provide a custom transition stub that calls the hook.
- Does stubbing Teleport change what `findComponent()` returns for the teleported child?No. `findComponent()` works on the component tree, which a teleport never changes, so it finds the child with or without the stub. What the stub changes is the DOM: the content sits inside the wrapper's element, so element-level `find()` and `html()` now include it.
Stubbing Teleport is like having a parcel delivered to your own desk instead of forwarding it to its real address: you can open and inspect it easily, but you never learn whether the forwarding address was right.
saying these in an interview costs you the question
- Vue Test Utils stubs Teleport by default, just like Transition.
- Tests must wait for the CSS leave duration before asserting a v-if inside Transition is gone.
- The transition stub still runs @after-leave and @enter hooks.
- wrapper.find() can reach teleported content because it searches the virtual tree.
- Stubbing Teleport also verifies that the teleport target selector is correct.