Testes E2E para Desktop Windows
Testes de ponta a ponta para aplicativos desktop nativos do Windows usando pywinauto com o backend Automação de Interface do Windows (UIA). Abrange WPF, WinForms, Win32/MFC e Qt (5.x/6.x) — com orientações específicas para Qt em uma seção dedicada.
Quando Ativar
- Escrevendo ou executando testes E2E para um aplicativo desktop nativo do Windows
- Configurando uma suíte de testes de GUI desktop do zero
- Diagnosticando testes de desktop automatizados instáveis ou com falha
- Adicionando testabilidade (AutomationId, nomes acessíveis) a um aplicativo existente
- Integrando testes E2E desktop em um pipeline CI/CD (GitHub Actions
windows-latest)
Quando NÃO Usar
- Aplicações Web → use a skill
e2e-testing(Playwright) - Aplicações Electron/CEF/WebView2 → a camada HTML precisa de automação de navegador, não UIA
- Aplicações Mobile → use ferramentas específicas da plataforma (UIAutomator, XCUITest)
- Testes de unidade ou integração puros que não precisam de uma GUI em execução
Conceitos Fundamentais
Toda automação de desktop Windows depende da Automação de Interface do Windows (UIA), uma API de acessibilidade nativa do Windows. Cada framework suportado expõe uma árvore de elementos UIA com propriedades que o agente pode ler e nas quais pode atuar:
Seu teste (Python)
└── pywinauto (backend UIA)
└── API de Automação de Interface do Windows ← nativa do Windows, independente de framework
└── Provedor UIA do aplicativo ← cada framework fornece o seu
└── .exe em execução
Qualidade UIA por framework:
| Framework | AutomationId | Confiabilidade | Notas |
|---|---|---|---|
| WPF | 5/5 | Excelente | x:Name mapeia diretamente para AutomationId |
| WinForms | 4/5 | Boa | AccessibleName = AutomationId |
| UWP / WinUI 3 | 5/5 | Excelente | Suporte completo da Microsoft |
| Qt 6.x | 5/5 | Excelente | Acessibilidade habilitada por padrão; nomes de classe mudam para Qt6* |
| Qt 5.15+ | 4/5 | Boa | Módulo de Acessibilidade melhorado |
| Qt 5.7–5.14 | 3/5 | Razoável | Precisa de QT_ACCESSIBILITY=1; objectName manual |
| Win32 / MFC | 3/5 | Razoável | IDs de controle acessíveis; correspondência de texto comum |
Configuração e Pré-requisitos
# Python 3.8+, apenas Windows
pip install pywinauto pytest pytest-html Pillow pytest-timeout
# Opcional: gravação de tela
# Instale o ffmpeg e adicione ao PATH: https://ffmpeg.org/download.html
Verifique se o UIA está acessível:
from pywinauto import Desktop
Desktop(backend="uia").windows() # lista todas as janelas de nível superior
Instale o Accessibility Insights for Windows (gratuito, da Microsoft) — sua ferramenta DevTools equivalente para inspecionar a árvore de elementos UIA antes de escrever qualquer teste.
Configuração de Testabilidade (por Framework)
A coisa mais impactante que você pode fazer é dar a cada controle interativo um AutomationId estável antes de escrever os testes.
WPF
<!-- XAML: x:Name torna-se AutomationId automaticamente -->
<TextBox x:Name="usernameInput" />
<PasswordBox x:Name="passwordInput" />
<Button x:Name="btnLogin" Content="Login" />
<TextBlock x:Name="lblError" />
WinForms
// Defina no designer ou no código
usernameInput.AccessibleName = "usernameInput";
passwordInput.AccessibleName = "passwordInput";
btnLogin.AccessibleName = "btnLogin";
lblError.AccessibleName = "lblError";
Win32 / MFC
// IDs de recurso de controle no arquivo .rc são expostos como strings AutomationId
// IDC_EDIT_USERNAME -> AutomationId "1001"
// Prefira SetWindowText para Name; adicione IAccessible para suporte mais rico
Qt — veja a seção dedicada abaixo
Modelo de Página (Page Object Model)
tests/
├── conftest.py # fixture de inicialização do aplicativo, captura de tela em falha
├── pytest.ini
├── config.py
├── pages/
│ ├── __init__.py # necessário para imports
│ ├── base_page.py # localizadores, espera, helpers de captura de tela
│ ├── login_page.py
│ └── main_page.py
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ └── test_main_flow.py
└── artifacts/ # capturas de tela, vídeos, logs
base_page.py
import os, time
from pywinauto import Desktop
from config import ACTION_TIMEOUT, ARTIFACT_DIR
class BasePage:
def __init__(self, window):
self.window = window
# --- Localizadores (ordem de prioridade) ---
def by_id(self, auto_id, **kw):
"""AutomationId — mais estável. Use como primeira escolha."""
return self.window.child_window(auto_id=auto_id, **kw)
def by_name(self, name, **kw):
"""Texto visível / nome acessível."""
return self.window.child_window(title=name, **kw)
def by_class(self, cls, index=0, **kw):
"""Classe de controle + índice — frágil, evite se possível."""
return self.window.child_window(class_name=cls, found_index=index, **kw)
# --- Esperas ---
def wait_visible(self, spec, timeout=ACTION_TIMEOUT):
spec.wait("visible", timeout=timeout)
return spec
def wait_gone(self, spec, timeout=ACTION_TIMEOUT):
spec.wait_not("visible", timeout=timeout)
return spec
def wait_window(self, title, timeout=ACTION_TIMEOUT):
"""Aguarda uma nova janela de nível superior (diálogos, janelas filhas)."""
dlg = Desktop(backend="uia").window(title=title)
dlg.wait("visible", timeout=timeout)
return dlg
def wait_until(self, fn, timeout=ACTION_TIMEOUT, interval=0.3):
"""Avalia uma condição arbitrária — use quando os eventos UIA não são confiáveis."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
if fn():
return True
except Exception:
pass
time.sleep(interval)
raise TimeoutError(f"Condição não atendida em {timeout}s")
# --- Ações ---
def click(self, spec):
self.wait_visible(spec)
spec.click_input()
def type_text(self, spec, text):
self.wait_visible(spec)
ctrl = spec.wrapper_object()
try:
ctrl.set_edit_text(text)
except Exception as e:
# Qt 5.x fallback: o Padrão de Valor UIA pode estar incompleto
import sys, pywinauto.keyboard as kb
print(f"[windows-desktop-e2e] set_edit_text falhou ({e}), usando fallback do teclado", file=sys.stderr)
ctrl.click_input()
kb.send_keys("^a")
kb.send_keys(text, with_spaces=True)
def get_text(self, spec):
ctrl = spec.wrapper_object()
for attr in ("window_text", "get_value"):
try:
v = getattr(ctrl, attr)()
if v:
return v
except Exception:
pass
return ""
# --- Artefatos ---
def screenshot(self, name):
os.makedirs(ARTIFACT_DIR, exist_ok=True)
path = os.path.join(ARTIFACT_DIR, f"{name}.png")
self.window.capture_as_image().save(path)
return path
login_page.py
from pages.base_page import BasePage
class LoginPage(BasePage):
@property
def username(self): return self.by_id("usernameInput")
@property
def password(self): return self.by_id("passwordInput")
@property
def btn_login(self): return self.by_id("btnLogin")
@property
def error_label(self): return self.by_id("lblError")
def login(self, user, pwd):
self.type_text(self.username, user)
self.type_text(self.password, pwd)
self.click(self.btn_login)
def login_ok(self, user, pwd, main_title="Main Window"):
self.login(user, pwd)
return self.wait_window(main_title)
def login_fail(self, user, pwd):
self.login(user, pwd)
self.wait_visible(self.error_label)
return self.get_text(self.error_label)
conftest.py
Para novos projetos, prefira a fixture sandbox Nível 1 (veja abaixo) — ela adiciona isolamento do sistema de arquivos sem custo extra. Esta fixture básica é apenas para configurações mínimas/legadas.
import os, pytest
os.environ["QT_ACCESSIBILITY"] = "1" # Necessário para suporte UIA no Qt 5.x
from pywinauto import Application
from config import APP_PATH, MAIN_WINDOW_TITLE, LAUNCH_TIMEOUT, ARTIFACT_DIR
@pytest.fixture
def app(request):
if not APP_PATH:
pytest.exit("A variável de ambiente APP_PATH não está definida", returncode=1)
proc = Application(backend="uia").start(APP_PATH, timeout=LAUNCH_TIMEOUT)
win = proc.window(title=MAIN_WINDOW_TITLE)
win.wait("visible", timeout=LAUNCH_TIMEOUT)
yield win
# Captura de tela em falha
if getattr(getattr(request.node, "rep_call", None), "failed", False):
os.makedirs(ARTIFACT_DIR, exist_ok=True)
try:
win.capture_as_image().save(
os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
)
except Exception:
pass
# Saída gracefull primeiro, encerramento forçado como fallback
# proc é um Application do pywinauto — use wait_for_process_exit(), não wait_for_process()
try:
win.close()
proc.wait_for_process_exit(timeout=5)
except Exception:
proc.kill()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())
config.py
import os
APP_PATH = os.environ.get("APP_PATH", "") # definido via env — sem caminho padrão
MAIN_WINDOW_TITLE = os.environ.get("APP_TITLE", "")
LAUNCH_TIMEOUT = int(os.environ.get("LAUNCH_TIMEOUT", "15"))
ACTION_TIMEOUT = int(os.environ.get("ACTION_TIMEOUT", "10"))
ARTIFACT_DIR = os.path.join(os.path.dirname(__file__), "artifacts")
pytest.ini
[pytest]
testpaths = tests
markers =
smoke: testes smoke rápidos para caminhos críticos
flaky: testes conhecidamente instáveis
addopts = -v --tb=short --html=artifacts/report.html --self-contained-html
Estratégia de Localizadores
AutomationId > Nome (texto) > ClassName + índice > XPath
(estável) (legível) (frágil) (último recurso)
Inspecione com Accessibility Insights → painel Propriedades → procure por AutomationId primeiro.
# Inspecione em tempo de execução — cole em um REPL para explorar a árvore
win.print_control_identifiers()
# ou limite o escopo:
win.child_window(auto_id="groupBox1").print_control_identifiers()
Padrões de Espera
# Aguarda o controle aparecer
page.wait_visible(page.by_id("statusLabel"))
# Aguarda o controle desaparecer (ex.: spinner de carregamento)
page.wait_gone(page.by_id("spinnerOverlay"))
# Aguarda um diálogo aparecer
dlg = page.wait_window("Confirmar Exclusão")
# Condição personalizada (ex.: mudança de texto)
page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "Pronto")
Nunca use time.sleep() como sincronização primária — use wait() ou wait_until().
Gerenciamento de Artefatos
# Captura de tela sob demanda
page.screenshot("apos_login")
# Captura de tela em tela cheia (quando a janela está fora da tela ou minimizada)
import pyautogui
pyautogui.screenshot("artifacts/fullscreen.png")
# Gravação de tela com ffmpeg (iniciar antes do teste, parar depois)
import subprocess
def iniciar_gravacao(nome):
return subprocess.Popen([
"ffmpeg", "-f", "gdigrab", "-framerate", "10",
"-i", "desktop", "-y", f"artifacts/videos/{nome}.mp4"
], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def parar_gravacao(proc):
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
Rastreamento por Etapa (opt-in)
A captura de tela padrão em falha é muitas vezes insuficiente para diagnosticar testes instáveis. O rastreamento em nível de etapa abaixo está desativado por padrão — ative-o apenas ao reproduzir um caso instável.
Ativar
E2E_TRACE=1 pytest tests/test_login.py -v
# Incluir texto digitado no log JSONL (NÃO use em testes que digitam credenciais/PII):
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...
Integrar no BasePage
import os, json, time
TRACE_ENABLED = os.environ.get("E2E_TRACE") == "1"
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"
class BasePage:
_step = 0
def _trace(self, action, spec=None, text=None):
if not TRACE_ENABLED:
return
BasePage._step += 1
idx = f"{BasePage._step:03d}"
os.makedirs(ARTIFACT_DIR, exist_ok=True)
try:
self.window.capture_as_image().save(
os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
except Exception:
pass # falha na captura não deve quebrar o teste
rec = {
"ts": time.time(), "step": BasePage._step, "action": action,
"locator": getattr(spec, "criteria", None),
"text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
}
with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
f.write(json.dumps(rec) + "\n")
def click(self, spec):
self.wait_visible(spec); self._trace("click_before", spec)
spec.click_input(); self._trace("click_after", spec)
def type_text(self, spec, text):
self.wait_visible(spec); self._trace("type_before", spec, text)
# ... lógica existente de set_edit_text / fallback de teclado ...
self._trace("type_after", spec)
Advertências
- PII / credenciais: o conteúdo de
type_texté<redacted>por padrão. Nunca definaE2E_TRACE_INCLUDE_TEXT=1em fluxos de login ou pagamento. - Sobrecarga: ~50–200ms por ação + um PNG por etapa em disco. Não ative na matriz CI padrão — apenas em um trabalho dedicado de reprodução de instabilidade.
- Inchaço de artefatos: um fluxo longo produz dezenas de MB; ajuste
retention-daysadequadamente. - Higiene de paralelismo/re-execução: este exemplo simples anexa a
trace.jsonle usa um contador em nível de classe. Limpe o diretório de artefatos antes de reexecuções e use diretórios de artefatos por worker para testes paralelos. - Lacuna de cobertura: ações executadas fora do
BasePage(chamadas diretas apywinautono código de teste) não são rastreadas.
Tratamento de Testes Instáveis
# Quarentena — equivalente a test.fixme() do Playwright
@pytest.mark.skip(reason="Instável: race condition de animação em CI lenta. Issue #42")
def test_transicao_animada(self, app): ...
# Pular se uma condição não for atendida (ex.: recurso ausente)
@pytest.mark.skipif("--run-slow" not in sys.argv, reason="Teste lento; pule com --run-slow")
def test_carga_pesada(self, app): ...
Gradação de Reinicialização (Tier 1 Sandbox)
Em CI, um aplicativo desktop pode vazar estado entre testes. Para evitar isso, o fixture Tier 1 Sandbox abaixo recria o diretório de configuração do aplicativo antes de cada teste:
import os, shutil, tempfile, pytest
from pywinauto import Application
SANDBOX = os.environ.get("E2E_SANDBOX", "")
@pytest.fixture
def app_sandbox(request):
"""
Fixture que cria um sandbox de sistema de arquivos isolado.
Define os.path.expanduser para um diretório temporário,
copia os dados de configuração necessários e inicia o aplicativo.
"""
sandbox_dir = tempfile.mkdtemp(prefix="e2e_sandbox_")
app_data_dir = os.path.join(sandbox_dir, "AppData")
os.makedirs(app_data_dir)
os.environ["USERPROFILE"] = sandbox_dir
# Copie os dados de configuração iniciais, se houver
# shutil.copytree("initial_config", app_data_dir, dirs_exist_ok=True)
proc = Application(backend="uia").start(APP_PATH, timeout=LAUNCH_TIMEOUT)
win = proc.window(title=MAIN_WINDOW_TITLE)
win.wait("visible", timeout=LAUNCH_TIMEOUT)
yield win
try:
win.close()
proc.wait_for_process_exit(timeout=5)
except Exception:
proc.kill()
finally:
# Limpeza do sandbox pode ser adiada ou omitida em CI
if not SANDBOX:
shutil.rmtree(sandbox_dir, ignore_errors=True)
Análise Pós-Morte (Post-Mortem) com Hooks do Pytest
# No conftest.py — adicione ao fixture 'app' existente
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
print(f"\n--- INÍCIO PÓS-MORTE: {item.name} ---")
print(f" Duração: {report.duration:.2f}s")
print(f" Causa: {report.longrepr}")
# Salve informações do sistema, se aplicável
print(f"--- FIM PÓS-MORTE ---")
Integração Contínua
GitHub Actions (windows-latest)
# .github/workflows/e2e-desktop.yml
name: Testes E2E Desktop
on: [push, pull_request]
jobs:
e2e:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Instalar dependências
run: |
pip install pywinauto pytest pytest-html Pillow pytest-timeout
- name: Baixar/binário do aplicativo
run: |
# Substitua pela URL real de download do seu aplicativo
Invoke-WebRequest -Uri "${{ secrets.APP_DOWNLOAD_URL }}" -OutFile "app_installer.exe"
.\app_installer.exe /S /D=${{ github.workspace }}\app
echo "APP_PATH=${{ github.workspace }}\app\MyApp.exe" >> $env:GITHUB_ENV
echo "APP_TITLE=MyApp" >> $env:GITHUB_ENV
- name: Executar testes E2E
run: |
pytest tests/ --html=artifacts/report.html --self-contained-html -v
- name: Fazer upload de artefatos
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-artifacts
path: artifacts/
Estratégia de Matriz para Múltiplos Binários
jobs:
e2e:
strategy:
matrix:
binary: [App-Stable.exe, App-Beta.exe]
steps:
- uses: actions/checkout@v4
- ... # instalar dependências
- name: Baixar binário: ${{ matrix.binary }}
run: |
Invoke-WebRequest -Uri "${{ secrets.APP_DOWNLOAD_URL }}${{ matrix.binary }}" -OutFile "${{ github.workspace }}\app.exe"
echo "APP_PATH=${{ github.workspace }}\app.exe" >> $env:GITHUB_ENV
echo "APP_TITLE=Aplicativo Teste" >> $env:GITHUB_ENV
- name: Executar testes E2E
run: pytest tests/ -v
Advertências e Observações
Ganchos de Janela e Antivírus
A automação UIA funciona via ganchos de janela do Windows. Alguns softwares antivírus (especialmente McAfee, Norton) ou ferramentas de produtividade (AutoHotkey, ferramentas de macro) podem interferir. Se suspeitar de interferência, execute o teste em um ambiente limpo (sandbox ou VM).
Permissões de Administrador
Se o aplicativo solicitar elevação de privilégios (executar como Administrador), o teste também precisará ser executado como Administrador. Isso é comum na CI apenas com configuração especial.
Ferramentas de Depuração de Acessibilidade
| Ferramenta | Propósito |
|---|---|
| Accessibility Insights | Inspecionar elementos UIA (equivalente ao DevTools do navegador) |
| Inspect.exe | SDK do Windows — leve, mostra a árvore UIA bruta |
| UISpy.exe | Alternativa open-source mais antiga |
Lidando com Ctrl+Alt+Del / Tela de Bloqueio
Se um teste acionar a tela de segurança do Windows (ex.: Ctrl+Alt+Del), os testes falharão porque a automação UIA não pode cruzar essa fronteira de segurança. Evite testar essa interação ou garanta que ela não seja acionada acidentalmente.
Orientações Específicas para Qt
Aplicativos Qt precisam de atenção especial porque a pilha de acessibilidade depende da versão e das variáveis de ambiente.
Qt 6.x (Recomendado)
- Acessibilidade habilitada por padrão.
- Nomes de classe mudam de
Qt5*paraQt6*(ex.:Qt6Edit,Qt6PushButton). objectNamemapeia paraAutomationId.- Funciona com o backend UIA do pywinauto sem configuração extra.
Qt 5.15+
- Defina
QT_ACCESSIBILITY=1antes de iniciar o aplicativo. objectNamegeralmente mapeia paraAutomationId, mas teste primeiro.- Alguns widgets podem precisar de subclasses para expor propriedades UIA corretas.
Qt 5.7–5.14
- Sempre defina
QT_ACCESSIBILITY=1(como variável de ambiente ou no código). objectNamenem sempre mapeia paraAutomationId; você pode precisar usar correspondência de texto.- Teste em um ambiente CI para garantir que o back-end UIA funcione.
- Se
set_edit_textfalhar, o fallback de teclado emBasePage.type_text()é usado automaticamente.
Exemplo: Adicionando objectName no Qt Designer / C++
// No construtor do seu widget ou diálogo
ui->usernameEdit->setObjectName("usernameInput");
ui->passwordEdit->setObjectName("passwordInput");
ui->loginButton->setObjectName("btnLogin");
ui->errorLabel->setObjectName("lblError");
Depuração Qt
# Verifique se o módulo de acessibilidade Qt está carregado
import os
print("QT_ACCESSIBILITY =", os.environ.get("QT_ACCESSIBILITY", "não definido"))
# Inspecione a árvore UIA do aplicativo Qt
win.print_control_identifiers()
Se a árvore UIA estiver vazia ou incompleta, verifique:
- A variável de ambiente
QT_ACCESSIBILITYestá definida como1? - O aplicativo está compilado com suporte a acessibilidade? (Por padrão em Qt 5.15+ e Qt 6.x)
- Você está usando o backend
"uia"do pywinauto? (vs. o backend"win32"padrão)
Migrando de pywinauto backend "win32" para "uia"
Se seu código existente usa o backend "win32" do pywinauto (o padrão), migrar para "uia" melhora muito a confiabilidade, especialmente com controles ricos. Veja o que muda:
| Recurso | win32 | uia |
|---|---|---|
| Localizador automático | auto_id não suportado | auto_id suportado (AutomationId) |
| Caixas de texto | set_edit_text lento | set_edit_text rápido |
| Tabelas/Listas | Suporte limitado | Suporte completo a padrões |
| Barras de menu | MenuSelect | Suporte a itens de menu via automação |
Para migrar:
- Mude
Application(backend="uia")em sua fixture. - Substitua chamadas
window_()porchild_window()comauto_id=onde possível. - Teste cada página — alguns localizadores podem precisar de ajustes devido a mudanças na árvore.
Convenções de Nomenclatura
| Item | Convenção | Exemplo |
|---|---|---|
| AutomationId | camelCase | btnLogin, usernameInput |
| Nome da página/classe | PascalCase + Page | LoginPage, MainPage |
| nome do método | snake_case | login_ok, wait_visible |
| Nome da fixture | snake_case, apenas app | app, app_sandbox |
| Nome do arquivo de configuração | snake_case | config.py |
| Marcador de teste | snake_case, texto curto | smoke, flaky |
Metadados do Projeto
- Versão: 1.0.0
- Licença: MIT
- Dependências: Python 3.8+,
pywinauto,pytest,Pillow - Dependências opcionais:
pytest-html,pytest-timeout,ffmpegpara gravação de tela
FAQ
Por que o set_edit_text falha no meu aplicativo Qt?
Isso geralmente acontece quando o Padrão de Valor UIA não está totalmente implementado. A classe BasePage.type_text() inclui um fallback automático de teclado para esses casos. Certifique-se de que QT_ACCESSIBILITY=1 está definido para Qt 5.x.
Como encontro o AutomationId de um controle?
Use o Accessibility Insights → clique em Inspect → clique no controle → procure no painel Propriedades por AutomationId. Se estiver vazio, o desenvolvedor precisa adicioná-lo no código-fonte.
Meu teste funciona localmente, mas falha no CI
Causas comuns:
- Variáveis de ambiente: o binário do aplicativo ou argumentos de linha de comando não estão presentes.
- Resolução de tela: CI pode ter uma resolução menor, movendo controles para fora da tela.
- Escala DPI: O Windows CI pode estar com escala de 150%; use
SetProcessDPIAware()no aplicativo ou configure o fixture de teste para DPI compatível. - Problemas de permissões: O aplicativo solicita elevação de administrador.
- Caminho temporal: O aplicativo não inicia dentro do
LAUNCH_TIMEOUT; aumente o timeout em CI.
Devo usar pywinauto ou pyautogui para cliques?
Use pywinauto para interagir com controles específicos (por exemplo, encontrar uma caixa de texto e digitar). Use pyautogui apenas para coordenadas de tela ou capturas de tela em tela cheia. Não misture ambos para localização de controles — pywinauto é mais confiável com UIA.