Cenários
Um cenário é um arquivo JSON no seu diretório de cenários (padrão e2e/scenarios/):
{
"scenario_id": "checkout",
"start_url": "/",
"task": "Log in as the qa account, add 'Backpack' to the cart, check out and verify the order confirmation message appears.",
"hints": ["Optional site-specific tips for the planner. Delete if not needed."]
}
start_urlé opcional (padrão/) e deve permanecer livre de ambiente: um caminho, resolvido contra a base URL efetiva.- Termine a tarefa com o que verificar — isso vira a pós-condição final do plano. Além de “um elemento está visível” ou “a URL é X”, o plano pode asserir condições mais ricas: o texto contém uma string, uma contagem de elementos (
equals/min/max), um seletor sumiu (não visível), ou um atributo igual a um valor — assim “verifique que aparecem 3 pedidos”, “verifique que o banner de erro some” ou “verifique que o campo fica marcado como válido” viram checagens precisas. Escreva a tarefa assim e o planejador emite oexpectcorrespondente. - Coloque entre aspas o texto que você quer verificar — o Windup o asserta por você. Quando a tarefa nomeia um literal entre aspas (“…e verifique que o texto ‘Popular parking’ aparece”) e a checagem final do plano acaba saindo fraca, o Windup a reescreve como um
text_containssobre exatamente esse texto — de forma determinística, sem nenhuma chamada extra ao LLM, e só depois de confirmar que a página ao vivo realmente o contém. Essa é a maneira confiável de obter uma asserção forte: ela não depende de o modelo escolher bem. - Uma verificação que não pode falhar é rejeitada. Uma checagem final não vale nada quando passa independentemente da funcionalidade: um landmark puro (
body,html,main,div,#root) ou qualquer seletor puro que case com mais de um elemento na página ao vivo (h2numa página com cinco títulos — apagar a seção inteira ainda deixa quatro). O Windup checa os dois — a lista de landmarks offline, a contagem de correspondências contra a página real no momento do planejamento — e rejeita um plano assim, re-planejando com um erro que diz quantos elementos casaram e o que asserir no lugar. Asserções de conteúdo valem em qualquer seletor:text_contains: { selector: "main", text: "Popular parking" }passa, porque o que ela afirma é o texto. Planos já em cache seguem sendo reproduzidos sem alteração; rodewindup explain <id>para identificar uma verificação fraca que você já tem (ele imprime⚠ weak verification: …). - Nunca coloque segredos nas tarefas. Referencie contas a partir do manifesto do projeto (veja Credenciais de teste); o plano usará
value_ref: "ENV:VAR"e o valor real é resolvido apenas em tempo de execução, nunca em cache. - Diálogos nativos & verificação não-toast. O Windup lida com diálogos nativos do navegador (
window.confirm/alert/prompt) que protegem ações destrutivas (arquivar, excluir, cancelar): o planejador adiciona"dialog": "accept"(ou"dismiss"para cancelar) à ação que abre o diálogo — caso contrário o diálogo é auto-dispensado e a ação silenciosamente não faz nada. Ele também direciona a verificação final para um sinal persistente (uma linha que desaparece, um rótulo alterado, uma URL) em vez de um toast/snackbar efêmero que some em segundos. - Diálogo padrão para o cenário inteiro (
on_dialog). Se um fluxo dispara a mesma confirmação em vários passos (exclusão em massa, guardas de “sair da página?”), defina"on_dialog": "accept"(ou"dismiss") uma única vez no cenário e um handler persistente responde a cada diálogo nativo durante toda a execução — sem precisar de umdialogpor ação. Odialogpor ação ainda funciona para casos pontuais; quandoon_dialogestá presente, ele assume. - Forçar uma interação por passo (
atomic_steps). Por padrão o planejador pode comprimir um revelar-e-agir numa única ação. Defina"atomic_steps": truee ele deverá emitir uma interação por ação — nunca mesclando um clique de expandir/abrir com o controle que ele revela — para que o replay fique granular e o relatório legível quando a UI esconde controles atrás de um disclosure. - Colocar um cenário flaky em quarentena (
quarantine). Defina"quarantine": truee o cenário ainda roda e reporta, mas uma falha não faz a suíte falhar (código de saída diferente de zero) — assim um flake teimoso deixa de bloquear o CI enquanto você o conserta, sem apagar o teste nem deixá-lo vermelho a cada build. É exibido de forma chamativa (uma linha🔶no console, um seloQUARANTINEDno relatório,quarantined: trueno JSON), nunca pulado em silêncio. Combine comwindup trends <id>para ver se estabilizou. - Fallback por rótulo de acessibilidade (automático). Quando o seletor CSS de um plano erra no replay, o Windup tenta o alvo de novo pelo seu nome acessível (a descrição da ação cruzada com label/placeholder/role) e age apenas quando exatamente um campo visível casa — recuperando-se de um seletor chutado e frágil sem re-planejar. O passo recuperado é sinalizado no relatório (
≈ found "<label>" by label …). Se nem o seletor nem o rótulo resolvem, a falha diz que o controle provavelmente não tem rótulo acessível (lacuna de a11y) e que você o ancore com um hint — assim uma execução quebrada vira também um achado de acessibilidade. - Determinismo por cenário (
network/clock). Os stubs de requisiçãonetworke o relógio congeladoclockque owindup.config.tsexpõe globalmente também podem viver em um único cenário — restritos àquela execução e mesclados sobre a config global, com o cenário vencendo. É assim que você testa um estado de erro sem colateral:{ "scenario_id": "erro-lista-500", "network": [{ "url": "v1/passports", "status": 500 }] }força um 500 nesse endpoint só aqui (os stubs globais continuam valendo; o cenário de listagem normal fica intacto). Oclockmescla campo a campo (onow/timezonedo cenário sobrescreve o global). Aplicado na criação do contexto, nunca cacheado — o plano é planejado contra a página com stub, então uma asserção sobre a UI de erro é estável. (Um cenário com seu próprionetwork/clockpula o prewarming do navegador — o contexto pré-aquecido só carrega a config global.) O erro que um stub produz deliberadamente fica fora da barreira dofailOn— um500de stub não dispara--fail-on-resource/--fail-on-5xx. failOnpor cenário. Abra uma exceção de saúde em runtime para um cenário em vez de cegar a suíte inteira:{ "scenario_id": "…", "failOn": { "resourceErrors": false } }, ou um"ignore"restrito ao cenário. Ele é mesclado sobre oconfig.failOnglobal — as barreiras booleanas assumem o valor do cenário quando definidas, e as listasignorese concatenam (o ruído global + o deste cenário). Assim uma URL que você só precisa silenciar em uma listagem deixa de ser uma entrada deignoreque todos os outros cenários também carregam.- Organize por pasta. Os cenários são descobertos recursivamente, então você pode agrupá-los em subpastas (
e2e/scenarios/contacts/list.json,e2e/scenarios/auth/login.json). Oscenario_idé a identidade —run --all, a suíte do vitest edepends_onresolvem todos por ele, independentemente do caminho do arquivo (ids duplicados são reportados).
Dependências entre cenários (depends_on)
Os fluxos raramente começam do zero — criar uma conta bancária exige estar logado. Declare os pré-requisitos e cada cenário permanece pequeno, focado e individualmente cacheável:
{
"scenario_id": "create-bank-account",
"depends_on": ["login"],
"task": "Already on the dashboard, open Settings > Bank accounts, create an account named 'Inter' and verify it appears in the list."
}
- As dependências rodam na mesma sessão do navegador, em ordem, cada uma com seu próprio cache — uma suíte aquecida faz replay da cadeia inteira com zero chamadas ao LLM.
- Sem um
start_url, o cenário dependente continua de onde a última dependência terminou — e no primeiro planejamento o LLM vê essa página real (o dashboard pós-login), em vez de planejar às cegas. - Cadeias funcionam (
login→select-company→create-account), ciclos são rejeitados, e uma dependência que falha faz a execução falhar com o tipodependencyantes mesmo de o cenário em si começar. - Cada dependência mantém sua própria autorreparação: se o plano em cache dela quebrar, ela replaneja e recacheia — os dependentes se beneficiam automaticamente.
- Os snapshots de sessão evitam repetir a cadeia (a grande alavanca de velocidade). Reexecutar um fluxo de login pela UI para cada cenário que depende dele é o custo dominante de tempo real de uma suíte em cache. O Windup captura o estado de saída de cada dependência — o
storageStatedo Playwright (cookies + localStorage) mais sua URL final — depois que ela roda, e em um replay em cache posterior restaura esse estado em um contexto novo e pula a reexecução da cadeiadepends_on(deps≈0ms, reportado comoreused_session_from). A execução restaurada ainda é verificada: se a sessão estiver obsoleta ou não tiver sido totalmente capturada, o Windup descarta o snapshot e volta a reexecutar a cadeia completa — sem falso positivo, sem chamada de LLM desperdiçada. Os snapshots ficam em.windup/state/(ignorados pelo git — eles contêm cookies/tokens de autenticação; nunca faça commit deles). - Autorreparação guiada. Um replanejamento informa ao planejador o seletor exato que falhou (“não o reutilize”), reenfatiza suas dicas e — com
--suggest— realimenta o replanejamento com o mesmo diagnóstico especializado que você leria, para que ele corrija em vez de repropor um seletor já refutado. Se um cenário continua replanejando sem estabilizar, o Windup avisa que o app provavelmente não tem um seletor estável (uma lacuna de acessibilidade) ou tem uma condição de corrida, em vez de repetir silenciosamente. Uma regressão detectada sempre tem prioridade sobre a autorreparação que vem depois: se o replanejamento também falhar (uma resposta truncada, uma chave inválida), a execução ainda reporta a falha de pós-condição como manchete — com o esperado/obtido — e rebaixa o problema do próprio replanejamento a uma linhanote:. O tropeço da ferramenta nunca fala mais alto que o bug que ela acabou de encontrar. - Editar a
taskde um cenário invalida seu plano em cache (um teste reescrito é um teste diferente).
windup new lida com dependências das duas formas: --depends-on login as declara explicitamente, e o LLM autor também as sugere por conta própria — ele vê todos os cenários existentes (id + task) e, quando a instrução pressupõe um estado que um deles produz (“já logado…”), emite depends_on automaticamente (filtrado mecanicamente contra ids reais de cenários — nunca inventado).
Pré-condições de dados (requires). depends_on captura uma dependência de cenário; requires documenta uma de dados — os dados de seed (semente) que um cenário assume: "requires": ["1 active attraction", "a paid order"]. É declarativo (o Windup o mostra no relatório para que uma falha causada por dados ausentes seja legível, e mapeia o ciclo criar→usar→arquivar) — para de fato semear os dados, use setup / suite.setup.
Tags (tags). Marque um cenário com "tags": ["smoke", "checkout"] e rode um subconjunto no CI com run --all --tag smoke — smoke a cada push, a suíte completa toda noite.
Reutilização isomórfica de planos (like)
Em escala, muitos cenários são o mesmo fluxo em uma rota/entidade diferente — criar um contato, criar um negócio, criar uma empresa acionam todos o mesmo formulário. Em vez de pagar uma chamada de planejamento ao LLM para cada um, um cenário pode reutilizar o plano já comprovado de outro:
{
"scenario_id": "deals-create",
"start_url": "/deals/new",
"task": "Type 'Big Deal' into the Name field and click Save; verify a new row appears.",
"like": { "scenario": "contacts-create", "set": { "Alice": "Big Deal" } }
}
like.scenarionomeia o cenário cujo plano em cache ativo é o modelo. O Windup o instancia para este cenário — estestart_url, elike.settroca quaisquer valores de preenchimento que difiram ("source literal" → "value to use here", aplicado apenas aos camposvalue; os seletores e os segredosvalue_refficam intactos).- O plano reutilizado ainda é executado e verificado antes de ser confiado e colocado em cache — exatamente a mesma barreira que todo plano passa. Se as páginas não forem de fato isomórficas (um seletor não corresponde, a verificação falha), o Windup recorre ao planejamento normal com o LLM. Ele nunca pula a verificação, então não pode produzir um falso verde silencioso.
- Quando verifica, a execução custou zero chamadas ao LLM e o cenário agora tem seu próprio plano em cache; as execuções seguintes são replays
$0comuns. - A origem precisa ter sido planejada uma vez primeiro (seu plano é o modelo). Em uma suíte onde a origem roda depois, o cenário
likesimplesmente planeja com o LLM naquela rodada e reutiliza na próxima — sem erro, apenas uma otimização perdida.
Reutilize planos inteiros com like; reutilize um bloco de ações entre fluxos de resto diferentes com um fragmento (windup fragment extract). Ambos mantêm a garantia determinista e verificada.
Fixtures do lado do cliente (seed)
Parte do estado vive inteiramente no navegador — um carrinho em localStorage, um dispositivo POS selecionado em sessionStorage. Construí-lo pela UI toda vez é lento e acopla o teste a esse fluxo. seed injeta esse estado antes de o plano rodar, de forma determinista e sem nenhuma chamada ao servidor:
{
"scenario_id": "cart-updates-quantity",
"start_url": "/checkout/cart",
"task": "Increase the first item's quantity to 3 and verify the total updates.",
"seed": {
"localStorage": { "cart": "[{\"id\":\"tkt-1\",\"qty\":2,\"price\":50}]" },
"sessionStorage": { "pos_device": "reader-7" }
}
}
- Semeado por origem (padrão: a origem de
start_url; sobrescreva comseed.origin) via um script de inicialização do Playwright que roda antes dos scripts da app, de modo que a página já carrega nesse estado. - Cada chave é definida apenas se estiver ausente — as mutações da própria app (um carrinho que o teste depois edita) nunca são sobrescritas em navegações posteriores.
- Não faz parte do plano em cache: roda em toda execução (incluindo replays
$0), de modo que cenários semeados permanecem deterministas. - Seguro para CI por construção: você alcança um estado do lado do cliente diretamente em vez de conduzir um fluxo que poderia chegar ao servidor. Ótimo para cenários de carrinho/checkout e POS.
Idempotência, setup e teardown
Um replay reexecuta o mesmo plano em cache com os mesmos valores — ideal para fluxos idempotentes (editar um registro fixo para um valor fixo, alternar e checar, ler/listar/filtrar). Ele não serve para um CREATE puro cujo recurso tem uma chave única não reutilizável: a primeira execução o cria, todo replay viola a restrição. Duas formas de cobrir escritas:
- Prefira cenários idempotentes — edite um registro de teste conhecido em vez de criar um novo; o replay é
$0e não deixa resíduo. - Hooks
setup/teardown— comandos de shell que rodam fora do plano em cache (ou seja, em todo replay), para fixtures ou limpeza (apagar de vez o que o teste criou, resetar via SQL/HTTP):
{
"scenario_id": "create-contact",
"task": "Open Contacts, create a contact with CPF 111.111.111-11 and verify it appears in the list.",
"setup": "psql \"$DATABASE_URL\" -c \"delete from contacts where national_id = '11111111111'\"",
"teardown": "psql \"$DATABASE_URL\" -c \"delete from contacts where national_id = '11111111111'\""
}
setup roda antes do cenário e de suas dependências (uma falha faz a execução falhar); teardown roda depois, sempre — passando ou falhando (uma falha é um aviso). São seus próprios comandos confiáveis (como o beforeEach/afterEach de um teste), rodam na raiz do projeto com o env do processo, e nunca entram no plano ou no cache.
Para o estado compartilhado por toda a suíte (semear um banco de dados de fixtures uma vez, iniciar um stub), use suite.setup / suite.teardown na configuração — eles rodam uma vez ao redor de run --all (o análogo de beforeAll/afterAll), enquanto os hooks por cenário cuidam do estado por teste.
Não escreva cenários à mão
Dois jeitos de criar um cenário sem escrever JSON:
- Autoria com
windup new— dê uma instrução vaga e o LLM escreve um cenário preciso e verificável a partir das telas reais do seu app. windup record— autoria por demonstração: dirija um navegador headful, marque o que verificar, finalize. O Windup escreve o cenário e cacheia o plano gravado para um replay $0.