---
title: "Testando componentes no Angular com Vitest: TestBed, jsdom e browser mode"
description: "Como os testes de componente do Angular se comportam de verdade no Vitest: o TestBed não muda, o DOM é jsdom (sem layout, sem CSS de verdade), e os specs que precisam de um browser real são os que vale a pena mover pro browser mode do Vitest."
deck: "Depois que você decidiu migrar, a surpresa é o quão pouco seus testes de componente mudam: [Vitest é o runner padrão desde o Angular v21](https://angular.dev/guide/testing), mas `TestBed` e `ComponentFixture` são a mesma API. O que muda é o DOM por baixo. O runner usa [jsdom](https://angular.dev/guide/testing), que não tem layout engine, então alguns specs precisam do [browser mode do Vitest](https://angular.dev/guide/testing/migrating-to-vitest) e de um browser real no lugar."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/vitest-component-testing-angular/"
lang: "pt-BR"
type: "article"
---

# Testando componentes no Angular com Vitest: TestBed, jsdom e browser mode

> Depois que você decidiu migrar, a surpresa é o quão pouco seus testes de componente mudam: [Vitest é o runner padrão desde o Angular v21](https://angular.dev/guide/testing), mas `TestBed` e `ComponentFixture` são a mesma API. O que muda é o DOM por baixo. O runner usa [jsdom](https://angular.dev/guide/testing), que não tem layout engine, então alguns specs precisam do [browser mode do Vitest](https://angular.dev/guide/testing/migrating-to-vitest) e de um browser real no lugar.

## O TestBed não mudou, o runner mudou

O medo quando um projeto troca de test runner é que todo spec de componente precise de reescrita. Com testes de componente, na maioria das vezes não é assim. O builder de Vitest do Angular roda o mesmo `TestBed`, o mesmo `TestBed.createComponent` e o mesmo `ComponentFixture` que você já usa. O corpo do spec que cria um componente, lê `componentInstance`, consulta `nativeElement` e verifica um texto é idêntico no Vitest.

O que o `ng new` mudou foi o aparato em volta desse corpo, não o corpo. No Angular v21 o test target é o builder `@angular/build:unit-test`, `describe`/`it`/`expect` vêm do `vitest/globals`, e os testes rodam num DOM simulado. A API de testar componente é a parte que continuou igual.

Então a pergunta útil não é "como reescrevo meus testes de componente", e sim "quais dos meus testes de componente tocam em algo que o DOM simulado não consegue fazer". Essa lista é bem mais curta, e é dela que este artigo trata.

*Um teste de componente, sem mudança no Vitest*

```ts
import { TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  it('incrementa a contagem no clique', async () => {
    const fixture = TestBed.createComponent(CounterComponent);
    await fixture.whenStable();

    const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
    button.click();
    await fixture.whenStable();

    expect(fixture.nativeElement.querySelector('.count').textContent).toContain('1');
  });
});
```

## O DOM por baixo dos seus testes é jsdom, não o Chrome

Tem um fato só que já explica a maioria das surpresas. Por padrão, o runner do Vitest não abre um browser: ele roda seu componente no jsdom, uma implementação do DOM em JavaScript. A doc do Angular diz direto: "To simulate the browser's DOM, Vitest uses a library called jsdom." O CLI também detecta o happy-dom sozinho e usa ele se você instalar, mas num projeto recém-criado com `ng new` o ambiente é o jsdom.

O jsdom dá conta de uma boa parte dos testes de componente: estrutura, conteúdo de texto, atributos, classes, handlers de evento, inputs e outputs, e a ligação entre template e classe. Nada disso precisa de layout engine. O problema só começa quando um teste faz ao DOM uma pergunta que o jsdom não sabe responder.

E o jsdom é honesto sobre o limite. Ele mesmo se descreve assim: "does not have the capability to render visual content, and will act like a headless browser by default". Não há layout de CSS, então `getBoundingClientRect()` e `offsetTop` devolvem zeros. Um `<canvas>` se comporta como uma `<div>`, a menos que você adicione o pacote `canvas` como peer dependency. Um teste que mede, faz scroll ou lê geometria computada está testando um browser que o jsdom não finge ser.

| O que o teste de componente precisa | jsdom | Por quê |
|---|---|---|
| Estrutura renderizada, texto, classes, atributos | Funciona | Árvore de DOM sem layout. |
| Clique, input, foco, eventos de output | Funciona | Eventos disparam sem um renderer. |
| Inputs, outputs, ligação template/classe | Funciona | Sem geometria de browser envolvida. |
| `getBoundingClientRect`, `offsetTop`, tamanhos | Devolve zeros | Sem layout engine para computar geometria. |
| CSS de verdade, media queries, estilos computados | Não aplica | O jsdom faz parse do CSS mas não o posiciona. |
| Desenho e leitura de pixel em `<canvas>` | Age como uma `<div>` | Precisa do pacote `canvas` como peer, e mesmo assim não é uma GPU. |
| Posição de scroll, geometria de IntersectionObserver | Pouco confiável | Depende do layout que o jsdom não roda. |

## Quando o jsdom não basta: o browser mode

Para os specs que falham na tabela acima, a resposta do Angular é o browser mode do Vitest: o mesmo teste com `TestBed`, mas renderizado num browser real conduzido por Playwright ou WebdriverIO. O guia de migração apresenta isso como a opção para "tests that rely on browser-specific APIs (like rendering) or for debugging". Você ativa por projeto instalando um provider e adicionando a opção `browsers` no target `test` do `angular.json`, ou passando `--browsers` na linha de comando.

O provider é uma dependência de verdade. O `@vitest/browser-playwright` te dá `chromium`, `firefox` e `webkit`; o `@vitest/browser-webdriverio` dirige o `chrome` e os outros via WebDriver. Os testes rodam com janela visível por padrão e mudam para headless quando a variável de ambiente `CI` está setada, que é o comportamento que você quer: olhar localmente, rodar quieto no pipeline.

Eu não viraria a suíte inteira para browser mode. Um browser real é mais lento para subir e mais pesado no CI que o jsdom, e a maioria dos specs de componente não ganha nada com ele. O critério que aplico é estreito: recorro ao browser mode só para os specs que leem geometria real ou desenham num canvas, e deixo o resto no jsdom. A ideia é ter dois ambientes por intenção, não um ambiente por medo do outro.

*Ativar o browser mode num projeto (provider Playwright)*

```bash
npm install --save-dev @vitest/browser-playwright
npx playwright install chromium

# No angular.json, dentro das options do target test:
#   "browsers": ["chromium"]
# Ou por rodada, sem editar o angular.json:
ng test --browsers=chromium
```

## Change detection no teste: detectChanges, OnPush e zoneless

A outra coisa que vale saber é como o teste avisa o Angular de que o estado mudou. O reflexo antigo é o `fixture.detectChanges()`. Ele ainda funciona, e para uma suíte grande que já existe a doc do Angular é explícita ao dizer que converter para outra coisa provavelmente "not worth the effort". Mas não é o padrão para onde o time aponta os testes novos.

A forma recomendada é `await fixture.whenStable()`. O raciocínio no guia de zoneless é que o `detectChanges()` "forces change detection to run when Angular might otherwise have not scheduled change detection", então o teste deixa de se parecer com produção. Deixar o Angular decidir quando sincronizar, e esperar a estabilidade, fica mais perto de como o componente se comporta quando vai para produção. Os exemplos deste artigo usam `whenStable()` por isso.

Se você testa um componente em modo zoneless, adicione `provideZonelessChangeDetection()` ao `TestBed.configureTestingModule`. Deixe esse provider de fora enquanto o zone.js ainda está nos seus polyfills: pedir zoneless com o zone.js ainda presente é o que faz o Angular lançar `NG0908: In this configuration Angular requires Zone.js`. Com OnPush, que é a estratégia padrão no Angular 22, a disciplina é a mesma: notifique o Angular da mudança (uma escrita de signal, um input, um evento) e espere a estabilidade, em vez de apelar para um `detectChanges()` manual que esconde uma notificação que faltou.

*Um teste de componente zoneless e OnPush*

```ts
import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { CartBadgeComponent } from './cart-badge.component';

describe('CartBadgeComponent (zoneless, OnPush)', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideZonelessChangeDetection()],
    });
  });

  it('mostra a contagem de itens vinda do signal input', async () => {
    const fixture = TestBed.createComponent(CartBadgeComponent);
    fixture.componentRef.setInput('count', 3);
    await fixture.whenStable();

    expect(fixture.nativeElement.textContent).toContain('3');
  });
});
```

## O que muda vindo do Karma

No Karma os testes rodavam num Chrome real que o launcher subia. Esse é o hábito a desaprender: por padrão, o Vitest não sobe. Um spec que passava no Karma porque o Chrome por acaso posicionou o DOM, mediu um elemento ou renderizou um canvas pode falhar no jsdom por um motivo que não tem nada a ver com o seu componente. Aí a saída não é desconfiar da migração, e sim reconhecer aquele spec como candidato a browser mode e mover só ele.

Os helpers de tempo são o outro hábito do Karma que não sobrevive à mudança. `fakeAsync`, `tick`, `flush` e `waitForAsync` se apoiam numa Zone que o builder do Vitest não monta em volta de cada teste, então lançam o erro de ProxyZone. Isso é um trabalho à parte, com seus dois caminhos, tratado em [fakeAsync e o erro ProxyZone](/pt/angular/angular-vitest-fakeasync-proxyzone-error). Para o lado de config da mudança, e por que o test target é uma linha, veja [De Karma para Vitest: a config que realmente roda](/pt/angular/karma-to-vitest-angular-config). E antes de tudo isso, [se vale migrar uma suíte específica](/pt/angular/move-angular-test-suite-to-vitest) é uma decisão por si só.

A API de testar componente sobreviveu à mudança quase intacta. O trabalho aqui não é reescrever testes, e sim separá-los pelo que o DOM simulado consegue responder e mandar a lista curta de specs de geometria e canvas para um browser real.

## Artefato reaproveitável: jsdom vs browser mode: qual ambiente para este teste de componente

- Comece pelo jsdom. Estrutura, texto, classes, atributos, eventos, inputs, outputs e a ligação template/classe rodam ali sem precisar de browser.
- Mova um spec para o browser mode quando ele lê geometria real: `getBoundingClientRect`, `offsetTop`, posição de scroll ou IntersectionObserver baseado em layout, tudo que o jsdom devolve como zeros.
- Mova um spec para o browser mode quando ele desenha ou lê de um `<canvas>`, ou depende de CSS computado e media queries que o jsdom faz parse mas nunca posiciona.
- Mantenha o browser mode por spec, não por suíte: instale o `@vitest/browser-playwright`, defina `browsers` no `angular.json`, e deixe rodar headless no CI enquanto o resto fica no jsdom.
- Em qualquer ambiente, notifique o Angular da mudança e use `await fixture.whenStable()` em vez de forçar `detectChanges()`; adicione `provideZonelessChangeDetection()` ao `TestBed` para componentes zoneless ou OnPush.

## Perguntas frequentes

### O TestBed ainda funciona com Vitest no Angular?

Funciona. O builder de Vitest do Angular roda a mesma API de `TestBed`, `TestBed.createComponent` e `ComponentFixture` de antes. Um spec de componente que cria um fixture, consulta `nativeElement` e verifica texto ou eventos não muda quando você troca o runner para Vitest. O que muda é o ambiente de DOM por baixo e os helpers de tempo, não a API de testar componente.

### Qual ambiente de DOM o runner Vitest do Angular usa por padrão?

jsdom. A doc do Angular diz que "to simulate the browser's DOM, Vitest uses a library called jsdom". O CLI detecta o happy-dom sozinho e usa ele se você instalar, mas um projeto recém-criado com `ng new` roda os testes em jsdom, que não tem layout engine e não renderiza conteúdo visual.

### Quando preciso do browser mode do Vitest em vez do jsdom para um componente Angular?

Quando o teste depende de algo que o jsdom não faz: layout e geometria reais (`getBoundingClientRect`, `offsetTop`, scroll, IntersectionObserver), CSS aplicado e media queries, ou desenho em canvas. O jsdom devolve zeros para layout e trata `<canvas>` como uma `<div>`. Para esses specs, instale um provider como o `@vitest/browser-playwright` e defina a opção `browsers` para eles rodarem num browser real.

### Devo chamar fixture.detectChanges() ou await fixture.whenStable() num teste Vitest?

O `fixture.detectChanges()` ainda funciona, e a doc do Angular diz que converter uma suíte grande existente para fora dele provavelmente não compensa. Para testes novos, prefira `await fixture.whenStable()`: o `detectChanges()` força o change detection quando o Angular podia não ter agendado, então o teste se distancia do comportamento de produção. Para componentes zoneless ou OnPush, notifique o Angular da mudança e espere a estabilidade.

### Como testo um componente Angular zoneless ou OnPush no Vitest?

Adicione `provideZonelessChangeDetection()` ao `TestBed.configureTestingModule`, dirija o componente por notificações reais (uma escrita de signal, um input via `setInput`, um evento) e use `await fixture.whenStable()`. Se o zone.js ainda está nos seus polyfills e você pede zoneless sem o provider, o Angular lança `NG0908: In this configuration Angular requires Zone.js`.

## Fontes consultadas

- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/api/core/testing/TestBed
- https://angular.dev/api/core/testing/ComponentFixture
- https://angular.dev/guide/zoneless
- https://angular.dev/api/core/provideZonelessChangeDetection
- https://vitest.dev/guide/browser/
- https://vitest.dev/guide/browser/playwright
- https://github.com/jsdom/jsdom
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b

## Leia também

- [Quando migrar uma suíte Angular para Vitest](https://andreramos.dev/pt/angular/move-angular-test-suite-to-vitest/)
- [De Karma para Vitest no Angular: a config que realmente roda](https://andreramos.dev/pt/angular/karma-to-vitest-angular-config/)
- [fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest](https://andreramos.dev/pt/angular/angular-vitest-fakeasync-proxyzone-error/)
