← Todos os artigos

Artigo

Como construir um robô que sobrevive a mudanças de UI

Por que automações por seletor quebram em produção, e três padrões — fingerprint, self-healing e vision LLM — que continuam rodando depois que a UI mudou.

Read in English →

Segunda-feira. O robô que baixa relatórios do ERP roda perfeito, como roda há seis meses. Terça-feira de manhã: tudo quebrado. Nenhuma mensagem útil — só ElementNotFound em loop, e uma fila de 200 jobs travada.

A causa? Um dev júnior renomeou um <div> no dia anterior. O seletor #download-btn-v2 virou #download-button-final. Três dias até alguém perceber, debugar, ajustar.

A automação não quebrou. O seletor quebrou. O resto continuava funcionando perfeitamente.

Essa cena se repete em toda empresa que tem automação em produção há mais de um ano. E a culpa quase nunca é do robô — é da forma como ele foi construído.

Por que seletores quebram (as razões reais)

Tutorial de automação foca em “como clicar no botão”. Pouco fala sobre por que aquele botão vai mudar. Mas vai. Em produção, sempre vai.

As causas que mais vejo em campo:

  • Iteração de UI — design system muda, classes são renomeadas
  • Frameworks reescrevem — migração de jQuery pra React, IDs viram hashes
  • A/B testing — metade dos usuários vê uma estrutura, metade vê outra
  • Internacionalização — texto do botão muda de “Baixar” pra “Download”
  • Acessibilidade — alguém adiciona aria-labels e refatora a estrutura
  • Redesign — a cada 18 a 24 meses, a UI inteira é repensada

Se seu robô depende de uma propriedade que pode mudar por qualquer um desses motivos, ele vai quebrar. A questão não é se — é quando.

A hierarquia da estabilidade

Toda biblioteca séria de automação (Playwright, Cypress, Selenium) chegou na mesma conclusão: como você seleciona o elemento importa mais do que qual elemento você seleciona.

Do pior pro melhor:

1. Posiçãobutton:nth-child(3)

// Quebra na próxima feature que adiciona um botão antes
await page.click("button:nth-child(3)");

Frágil em todos os sentidos. Qualquer mudança estrutural quebra.

2. ID gerado#download-btn-v2-final

// Quebra na próxima vez que o time renomear ou regenerar
await page.click("#download-btn-v2-final");

Parece estável, mas IDs costumam ser “documentação não escrita” — qualquer dev pode renomear porque ninguém disse que era contrato.

3. Classe CSS.btn-download

// Quebra em qualquer refactor de design system
await page.click(".btn-download");

Classes existem pra estilo, não pra automação. Designer vai mexer.

4. Atributo dedicado[data-testid="download-receipt"]

// Estável SE o time mantiver a convenção
await page.click('[data-testid="download-receipt"]');

Padrão recomendado pelo Playwright e Cypress. Requer cooperação do time de produto — o que, em sistemas legados, você quase nunca tem.

5. Papel semânticogetByRole

// Sobrevive a refactor de CSS, mudança de ID, redesign visual
await page.getByRole("button", { name: /baixar recibo/i }).click();

A subestimada. Papéis ARIA descrevem o que o elemento faz, não como ele é. Sobrevivem à maioria dos refactors porque qualquer site acessível precisa preservá-los.

6. Combinação semântica + contexto — fingerprint

// Encontra o botão dentro do card de recibo, pelo papel + label
await page
  .locator("article", { hasText: "Recibo de fevereiro" })
  .getByRole("button", { name: /baixar/i })
  .click();

Vários sinais combinados. Pra cada um quebrar, vários teriam que mudar ao mesmo tempo — improvável.

7. Detecção visual / por vision LLM

# A última fronteira. Sobrevive a quase tudo. Lento e caro.
action = computer_use.find("o botão que baixa o recibo de fevereiro")

Modelos como Anthropic Computer Use enxergam a tela como um humano. Sobrevivem a praticamente qualquer mudança. Mas: ~10× mais lento, ~100× mais caro, e não-determinístico.

Os três padrões que sobrevivem na produção

A hierarquia acima é sobre o seletor individual. Mas seletor bom não é suficiente — você precisa de uma arquitetura que aceita falha.

Padrão 1: Fingerprint detection

Em vez de um seletor, vários sinais. Texto + papel + posição relativa + contexto pai. A função retorna o elemento que casa com a maioria dos sinais, não com todos.

function findDownloadButton(page) {
  return page
    .locator("*", { hasText: /baixar/i })
    .filter({ has: page.getByRole("button") })
    .filter({ hasText: /recibo|extrato/ })
    .first();
}

Quando o site muda, normalmente um sinal quebra. Os outros ainda batem. O robô continua.

Padrão 2: Self-healing

Tenta o seletor primário. Se falha, tenta uma cadeia de fallbacks. Quando um fallback funciona, salva como o novo primário.

const strategies = [
  () => page.getByTestId("download-receipt"),
  () => page.getByRole("button", { name: /baixar recibo/i }),
  () => page.locator("button", { hasText: /baixar/i }).first(),
];

for (const strategy of strategies) {
  try {
    await strategy().click();
    return;
  } catch {
    continue;
  }
}
throw new Error("Nenhuma estratégia funcionou — UI mudou demais");

Bonus: logue qual estratégia funcionou. Em uma semana você sabe quais seletores estão “envelhecendo”.

Padrão 3: Vision-based reasoning

Use vision-LLM como fallback de último recurso, não como padrão primário. Quando os outros padrões falham, mande o screenshot pro modelo, deixe ele identificar o elemento, e aprenda o novo seletor a partir disso.

Custo: alto por chamada. Frequência: baixa. ROI: positivo se a alternativa for um humano fazer manutenção semanal.

A economia da manutenção

Aqui é onde a maioria dos times se engana. “Vou fazer um robô rápido, seletor por CSS, em 2 horas. Se quebrar eu arrumo.”

Vamos somar:

  • 10 robôs frágeis × 30 min de manutenção por semana = 5h/semana
  • 5h × 52 semanas = 260h/ano (≈ 1,5 mês de trabalho)
  • A R$ 150/h carregado = R$ 39.000/ano em manutenção

Compare:

  • 3 robôs robustos × 10 min/mês de monitoramento = 6h/ano
  • 6h × R$ 150 = R$ 900/ano

A diferença não é o custo de construir. É o custo de manter, multiplicado por 12 meses.

Onde a IA entra (e onde atrapalha)

Vision-based agents (Computer Use, browser-use, Operator) são impressionantes. Mas têm um trade-off duro:

  • Latência — 3 a 10 segundos por ação, contra 50 a 200ms de um seletor
  • Custo — centavos a R$ 0,50 por ação, contra ~zero
  • Determinismo — o modelo pode “ver” o elemento errado em 1 a 2% das tentativas

Use quando: o fluxo roda poucas vezes por dia, o site muda muito, e a alternativa é manutenção humana semanal.

Não use quando: o fluxo processa milhares de itens, latência importa, ou o custo agregado supera o salário de um analista.

Voltando ao caso real

No primeiro artigo eu mencionei “o pulo do gato” — o robô que detectava o padrão de download em vez de hardcodar o seletor. Agora você tem o vocabulário pra entender o que era tecnicamente: fingerprint detection (Padrão 1) + self-healing (Padrão 2) combinados.

Não tinha vision na época. Hoje eu adicionaria um terceiro fallback de Computer Use pros casos onde os primeiros dois falham — vira a “rede de segurança final” e libera o time humano de qualquer manutenção rotineira.

A regra que separa script de sistema

Automação que quebra é automação que você continua pagando todo mês — em horas-pessoa, em fila atrasada, em cliente irritado. Automação robusta tem custo inicial maior e custo contínuo quase zero.

Se o seu robô já quebrou duas vezes esse ano por mudança de UI, ele não está em produção. Ele está em “produção até a próxima feature do time de produto”.

Se você quer conversar sobre transformar um robô frágil em um robô que dorme tranquilo, a primeira conversa é curta, sem custo. ~30 minutos.