mozak.tech Engenharia de IA Corporativa 58%

Parte III — Inteligência Artificial na Experiência do Usuário

3.4 — Automação de Testes e Interações de Interface com Agentes (UI Automation)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar um agente LLM que navega UI via tool calls (navegar, clicar, digitar, verificar)

Gerar casos de teste a partir de uma user story usando Claude (TODO 1)

Verificar o MCP context de uma aplicação web (TODO 2)

Comparar abordagem baseada em seletores CSS (frágil) vs instruções em linguagem natural (robusta)

Projetar pipelines de QA automatizado com agentes LLM

Por que isso importa

Testes de UI são o maior ponto de fricção em CI/CD. Seletores CSS quebram com qualquer mudança de markup. IDs mudam. data-testid precisa ser mantido pelos devs. O resultado: suítes de testes que o time para de manter.

A abordagem de agente LLM resolve diferente: em vez de page.click('#btn-submit'), você diz 'Clique no botão de enviar o formulário'. O agente interpreta a instrução, encontra o elemento correto e executa. Se o design mudar mas o fluxo for o mesmo, o teste continua funcionando.

Esse é o padrão que a Anthropic, Microsoft (Playwright AI) e Browserbase estão desenvolvendo em 2024-2025.

Conceitos Fundamentais

Tools do Agente UI

O starter define 5 tools que mapeiam operações de browser:

const UI_TOOLS: Anthropic.Tool[] = [

  {

    name: 'navegar_para',

    description: 'Navega para uma URL.',

    input_schema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },

  },

  {

    name: 'clicar_elemento',

    description: 'Clica em um elemento descrito em linguagem natural.',

    // description é o guia: "botão de enviar", "link Sair", "checkbox Aceito os Termos"

  },

  {

    name: 'digitar_texto',

    description: 'Digita texto em um campo de input.',

  },

  {

    name: 'capturar_screenshot',

    description: 'Captura screenshot da página atual.',

  },

  {

    name: 'verificar_elemento',

    description: 'Verifica se um elemento existe e tem o conteúdo esperado.',

  },

]

O agente LLM decide sozinho quando usar cada tool baseado na instrução em linguagem natural. Para “Faça login com email test@example.com”, ele vai: 1. navegar_para('/login') 2. digitar_texto({campo: 'email', texto: 'test@example.com'}) 3. digitar_texto({campo: 'senha', texto: '123456'}) 4. clicar_elemento({descricao: 'botão Entrar'}) 5. verificar_elemento({descricao: 'dashboard', conteudo_esperado: 'Bem-vindo'})

TODO 1: Gerar Casos de Teste a partir de User Story

async function gerarCasosDeTest(userStory: string): Promise<string[]> {

  const response = await client.messages.create({

    model: 'claude-haiku-4-5-20251001',

    max_tokens: 800,

    messages: [{

      role: 'user',

      content: `Dado esta user story, gere uma lista de instruções de teste em linguagem natural.

      

USER STORY: ${userStory}



Retorne JSON: {"casos": ["instrução 1", "instrução 2", ...]}



Cada instrução deve ser clara e acionável para um agente de automação.

Inclua: caminho feliz, validações de campo, casos de erro.

Máximo 8 casos.`,

    }],

  })

  

  try {

    const texto = response.content[0].text

    const i = texto.indexOf('{')

    const f = texto.lastIndexOf('}') + 1

    const parsed = JSON.parse(texto.slice(i, f))

    return parsed.casos || []

  } catch {

    return []

  }

}

Exemplo de input/output:

Input: "Como usuário, quero fazer login para acessar minha conta"

Output:

{

  "casos": [

    "Navegue para /login e verifique que o formulário de login está visível",

    "Digite email válido 'user@test.com' e senha '123456' e clique em Entrar",

    "Verifique que foi redirecionado para /dashboard após login",

    "Tente fazer login com senha errada e verifique mensagem de erro",

    "Teste com email inválido (sem @) e verifique validação do campo",

    "Clique em 'Esqueci minha senha' e verifique que o formulário de recuperação aparece"

  ]

}

TODO 2: Verificar MCP Context da Aplicação

async function verificarMCPContext(url: string): Promise<Record<string, unknown>> {

  // Apps com MCP expõem capabilities em endpoints padrão

  const endpoints = [

    '/.well-known/mcp',

    '/api/mcp',

    '/mcp/tools',

  ]

  

  for (const endpoint of endpoints) {

    try {

      const response = await fetch(`${url}${endpoint}`)

      if (response.ok) {

        const data = await response.json()

        return data

      }

    } catch {

      // endpoint não existe

    }

  }

  

  return {}  // app não expõe MCP context

}

Este é um padrão emergente: aplicações web expondriam suas capabilities via endpoint MCP, permitindo que agentes as descubram automaticamente.

Aprofundamento Técnico

Playwright Real vs Mock

O starter usa um executor mock (executarToolUI). Para conectar ao Playwright real:

import { chromium, Page } from 'playwright'



let page: Page



async function inicializarBrowser() {

  const browser = await chromium.launch({ headless: false })  // false = visível para debug

  const context = await browser.newContext()

  page = await context.newPage()

}



async function executarToolUI(nome: string, args: Record<string, string>): Promise<string> {

  switch (nome) {

    case 'navegar_para':

      await page.goto(args.url)

      return `Navegado para ${args.url}. Título: "${await page.title()}"`

    

    case 'clicar_elemento':

      // LLM descreve em NL → Playwright usa getByRole/getByText/getByLabel

      try {

        // Tenta por texto primeiro

        await page.getByText(args.descricao).first().click({ timeout: 5000 })

      } catch {

        // Fallback: getByRole

        await page.getByRole('button', { name: args.descricao }).click({ timeout: 5000 })

      }

      return `Clicado: "${args.descricao}"`

    

    case 'digitar_texto':

      await page.getByLabel(args.campo).fill(args.texto)

      return `Digitado "${args.texto}" em "${args.campo}"`

    

    case 'capturar_screenshot':

      await page.screenshot({ path: `screenshots/${args.nome_arquivo}.png` })

      return `Screenshot: screenshots/${args.nome_arquivo}.png`

    

    case 'verificar_elemento':

      const elemento = await page.getByText(args.descricao).first()

      const visivel = await elemento.isVisible()

      return visivel ? `✓ "${args.descricao}" encontrado` : `✗ "${args.descricao}" NÃO encontrado`

  }

}

getByRole, getByText, getByLabel são os seletores semânticos do Playwright — muito mais robustos que seletores CSS. O LLM usa descrições que mapeiam naturalmente para esses seletores.

Pipeline de QA Automatizado

async function rodarSuiteQA(app: string, userStories: string[]) {

  const todosResultados = []

  

  for (const story of userStories) {

    console.log(`\n=== Story: ${story.slice(0, 60)} ===`)

    

    // 1. Gerar casos de teste da story

    const casos = await gerarCasosDeTest(story)

    console.log(`  ${casos.length} casos gerados`)

    

    // 2. Executar cada caso

    for (const caso of casos) {

      console.log(`  Executando: ${caso.slice(0, 80)}`)

      try {

        await agenteUI(caso)

        todosResultados.push({ story, caso, status: 'passou' })

      } catch (err) {

        todosResultados.push({ story, caso, status: 'falhou', erro: err.message })

      }

    }

  }

  

  // 3. Relatório

  const passou = todosResultados.filter(r => r.status === 'passou').length

  const falhou = todosResultados.filter(r => r.status === 'falhou').length

  console.log(`\n=== RELATÓRIO: ${passou} passed, ${falhou} failed ===`)

  return todosResultados

}

Exemplos Anotados

Exemplo 1: Trace do Agente para “Fazer Login”

[AGENTE UI] Instrução: "Faça login com email test@example.com e senha 123456"



[BROWSER] navegar_para({"url": "https://app.exemplo.com/login"})

  ← Navegado para https://app.exemplo.com/login. Título: "Login"



[BROWSER] digitar_texto({"campo": "email", "texto": "test@example.com"})

  ← Digitado "test@example.com" em "email"



[BROWSER] digitar_texto({"campo": "senha", "texto": "123456"})

  ← Digitado "123456" em "senha"



[BROWSER] clicar_elemento({"descricao": "botão Entrar"})

  ← Clicado: "botão Entrar"



[BROWSER] verificar_elemento({"descricao": "dashboard", "conteudo_esperado": "Bem-vindo"})

  ← ✓ "dashboard" encontrado. Conteúdo: "Bem-vindo verificado"



[RESULTADO] Login realizado com sucesso. Dashboard carregado com mensagem de boas-vindas.

Exemplo 2: TODO 1 em Uso

const story = 'Como usuário, quero fazer login para acessar minha conta'

const casos = await gerarCasosDeTest(story)

// Resultado:

// [

//   "Navegue para /login e verifique que os campos email e senha estão visíveis",

//   "Digite email válido test@example.com e senha 123456 e clique em Entrar",

//   "Verifique redirecionamento para /dashboard com mensagem de boas-vindas",

//   "Tente login com email não cadastrado e verifique mensagem 'Email não encontrado'",

//   "Tente login com senha errada e verifique mensagem 'Senha incorreta'",

//   "Deixe campos vazios e clique Entrar — verifique mensagens de validação",

// ]



for (const caso of casos) {

  await agenteUI(caso)

}

Padrões e Armadilhas

Padrões

Padrão 1: Tool de verificação sempre ao final

// Instrução para o agente sempre terminar com verificação

'Ao final, use verificar_elemento para confirmar que a ação foi bem-sucedida.'

Padrão 2: Descrições de elemento específicas mas flexíveis

// Muito específico (frágil): "botão com classe btn-submit-v2"

// Muito vago: "o botão"

// Correto: "botão Entrar do formulário de login"

Padrão 3: Screenshot antes e depois de ações críticas

await agenteUI('Capture screenshot antes do pagamento, processe pagamento, capture screenshot do recibo')

Armadilhas

⚠️ Armadilha 1: json.loads sem tratamento de JSON parcial Claude às vezes retorna JSON com texto antes/depois. Use extração robusta:

const i = texto.indexOf('{'), f = texto.lastIndexOf('}') + 1

const data = JSON.parse(texto.slice(i, f))

⚠️ Armadilha 2: Agente sem limite de iterações tenta indefinidamente O starter usa MAX_ITER = 8. Sem limite, uma instrução ambígua pode gerar loop.

⚠️ Armadilha 3: Playwright headless: true em CI, headless: false em debug

const headless = process.env.CI === 'true'

const browser = await chromium.launch({ headless })
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

2 TODOs no starter:

TODO 1 — gerarCasosDeTest(userStory):

const response = await client.messages.create({

  model: 'claude-haiku-4-5-20251001',

  max_tokens: 800,

  messages: [{

    role: 'user',

    content: `User story: ${userStory}\nGere casos de teste em JSON: {"casos": ["caso 1", "caso 2", ...]}`,

  }],

})

const texto = response.content[0].text

const i = texto.indexOf('{'), f = texto.lastIndexOf('}') + 1

return JSON.parse(texto.slice(i, f)).casos || []

TODO 2 — verificarMCPContext(url): implementar fetch para os endpoints /.well-known/mcp, /api/mcp, /mcp/tools e retornar o primeiro que responder 200.

Rode main() e observe o agente navegando pelos 3 cenários de demo. O mock produz outputs realistas sem precisar de browser real.

Agora você está pronto para o lab.