Vue 3 changes more than syntax: multiple root nodes, Teleport, and different async update timing can break tests that assumed a flat wrapper or relied on a specific data-* query from the Vue 2 era. Cypress Component Testing (mounting a SFC in isolation) is not a replacement for unit tests with Vue Test Utils, but it is the right layer when you want a real browser without booting the whole app shell.
The goal of this note is a pragmatic split: what belongs in CT vs E2E, and what we fix first when a suite that was “green on Vue 2” turns into a wall of red on Vue 3.
1. Why flakiness spikes right after the upgrade
Most “flakes” are not random. They are tests that were accidentally synchronized to Vue 2’s update timing, or to a DOM structure that no longer exists. Teleport moves content; multiple roots break assumptions about .get('#app') children; async components resolve in a different order under Vite than they did under Webpack in dev.
The fix is to treat selectors and async boundaries as part of the migration, not as “test cleanup later.” Storybook can help lock visual and structural contracts before you invest in a large Cypress CT matrix.
2. A sensible pyramid after migration
- Unit + VTU: pure logic, composables, Pinia stores, and small presentational components with minimal DOM.
- Cypress CT: complex widgets (data grids, date pickers, wizards) where layout and browser APIs matter.
- E2E: cross-route flows, auth, and integrations—keep this layer thin if CT covers your component edge cases.
3. Practical upgrade checklist
- Pin
data-cyhooks to user-meaningful elements, not animation wrappers. - Replace implicit waits with retry-friendly assertions (Cypress already leans this way—double down).
- Stub network at the component boundary; avoid full API simulation in every CT.
- Run CT in the same build target you ship (legacy vs modern browsers) if you still support older clients.
4. Configuring Cypress CT for Vue 3 + Vite
The default scaffold is mostly correct, but two pieces trip teams up: the bundler entry and the global plugin registration used by the app. Cypress mounts a single component, not the whole app, so any plugin that the component implicitly depends on (Pinia, i18n, router) needs to be wired in the mount helper.
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,tsx}',
},
})Then the shared mount helper applies the plugins each suite expects. This is the equivalent of the global setup file you used to maintain for Vue Test Utils:
// cypress/support/component.ts
import { mount } from 'cypress/vue'
import { createPinia } from 'pinia'
import { createI18n } from 'vue-i18n'
import en from '@/locales/en.json'
Cypress.Commands.add('mount', (component, options = {}) => {
options.global = options.global || {}
options.global.plugins = [
...(options.global.plugins || []),
createPinia(),
createI18n({ legacy: false, locale: 'en', messages: { en } }),
]
return mount(component, options)
})If you are mid-migration to Pinia or still juggling vue-i18n locales, wire the active store version here and avoid mocking what the component actually needs.
5. Selectors that survive a refactor
The biggest source of red builds after Vue 3 is selectors that rely on DOM structure rather than user-meaningful targets. Replacing structural selectors with role and label-based ones is the single highest-leverage cleanup.
| Brittle | Resilient |
|---|---|
cy.get('.btn-primary > span') | cy.findByRole('button', {name: /save/i}) |
cy.get('div:nth-child(2) input') | cy.findByLabelText('Email') |
cy.get('.modal .close') | cy.findByRole('dialog').findByRole('button', {name: /close/i}) |
cy.get('[data-v-abc123]') | cy.get('[data-cy="invoice-row"]') |
cypress-testing-library pairs naturally with Vue 3 because it leans on accessibility semantics, which Vue's <Teleport> and multi-root components preserve even when DOM structure shifts.
6. Handling Teleport, fragments, and async setup
Three Vue 3 features cause the majority of post-migration failures and each has a clean answer.
Teleport
Teleported content (modals, toasts, dropdowns) renders outside the mounted component's root. Use cy.get('body') as the search root, or query by role across the document:
cy.mount(<ConfirmDialog open />)
cy.findByRole('dialog').within(() => {
cy.findByRole('button', {name: /confirm/i}).click()
})Fragments / multiple roots
wrapper.element from Vue 2 days no longer points to a single node. Avoid asserting on the wrapper's outer HTML; assert on visible content.
Async setup()
If a component uses <Suspense>, mount it inside its boundary. Cypress retries assertions automatically, so prefer cy.findByText(...) over manual cy.wait().
7. CT vs E2E: a decision matrix
| Scenario | Best layer | Why |
|---|---|---|
| Validate form validation rules | CT | Fast, deterministic, no auth needed |
| Date picker keyboard navigation | CT | Real browser, isolated component |
| Login → dashboard happy path | E2E | Cross-route, real cookies, real backend |
| Pure composable logic | VTU/Vitest | No DOM needed; runs in milliseconds |
| Visual regression across themes | Storybook + Chromatic | Built for this; CT does not snapshot well |
8. Anti-patterns we still see in 2026
- Manual
cy.wait(500)everywhere. Replace with retried assertions; Cypress already retries most queries until the command timeout. - Mounting
App.vuein CT. That is an E2E test in disguise—and a slow one. CT should mount one component plus its required plugins. - Snapshot-only assertions. Brittle across class hash changes from
scopedstyles. Assert on visible behavior instead. - Re-implementing E2E coverage in CT. CT is a complement to E2E, not a replacement. Keep the migration checklist honest about what each layer covers.
- Skipping accessibility checks.
cypress-axeon the most reused components catches more bugs than a third snapshot ever will.
9. FAQ
Should I rewrite tests during the migration or after?
Both. Delete tests that asserted Vue 2 internals. Port the ones tied to user behavior. Add new CT for primitives you are about to refactor under Composition API.
Vitest or Cypress CT for components?
If you mostly need DOM assertions and fast feedback, Vitest with happy-dom is excellent. Cypress CT pays off when the bug surface depends on real browser layout, scroll, focus, and keyboard handling.
How do I prevent flake from re-emerging?
Track flake rate per spec in CI. Quarantine the worst offender each sprint and fix or delete. The compounding effect outpaces any single tooling change.
Do I need @testing-library/vue?
For Vitest yes, often. For Cypress, the cypress-testing-library bridge gives you the same query semantics and is worth the install.
Stabilize tests while you finish the migration
We help teams triage which suites to rewrite, which to delete, and which to protect with a thin layer of new component tests that match Vue 3’s behavior.
Talk through your test planConclusion
Cypress component tests are most valuable in the window right after Vue 3 lands—when your team is touching the most UI code anyway. Invest in stable selectors and a clear split with E2E, and you will spend less time chasing flakes and more time shipping the migration.
Related guides
Vue Test Utils & Jest in 2026
Unit testing strategy when components move to Composition API.
Storybook 8 as a safety net
Visual and structural baselines before E2E and CT investment.
Migration checklist
Where testing phases fit in the overall plan.
Vue migration blockers
When test debt is the reason the upgrade keeps stalling.
