Automated browser testing interface with component test output
8 min read Vue 3 Migration

Cypress Component Testing After Vue 3: Rebuilding Confidence When the DOM Stops Behaving

End-to-end suites catch regressions, but they are slow and overkill for refactored UI primitives. After Vue 3, component tests are the fastest way to prove that buttons, modals, and forms still match expectations—if you adapt your toolkit to the new render tree.

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

  1. Pin data-cy hooks to user-meaningful elements, not animation wrappers.
  2. Replace implicit waits with retry-friendly assertions (Cypress already leans this way—double down).
  3. Stub network at the component boundary; avoid full API simulation in every CT.
  4. 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.

BrittleResilient
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

ScenarioBest layerWhy
Validate form validation rulesCTFast, deterministic, no auth needed
Date picker keyboard navigationCTReal browser, isolated component
Login → dashboard happy pathE2ECross-route, real cookies, real backend
Pure composable logicVTU/VitestNo DOM needed; runs in milliseconds
Visual regression across themesStorybook + ChromaticBuilt 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.vue in 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 scoped styles. 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-axe on 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 plan

Conclusion

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