IOI-RUN / auto_capture.py
Roudrigus's picture
Upload 82 files
0f0ef8d verified
# -*- coding: utf-8 -*-
"""
auto_capture.py — Captura screenshots de todas as telas do app Streamlit e monta um PPTX.
Recursos:
• Login automático (usuário/senha + escolha do banco)
• Bypass do Quiz (clica: “Voltar ao sistema”, “Finalizar”, “Continuar”, se visível)
• Seletores robustos para st.selectbox (procura pelo label visível)
• Captura pós-login/pós-quiz, por grupo e por módulo
• Artefatos de debug (HTML + PNG) quando algo falha
• Sanitização de nomes de arquivo (compatível com Windows)
• Geração de PPTX com um slide por módulo capturado
Requisitos:
pip install playwright python-pptx python-dotenv
playwright install
"""
import os
import re
import traceback
from datetime import datetime
from dotenv import load_dotenv
# Carrega .env
load_dotenv()
APP_URL = os.getenv("APP_URL", "http://localhost:8501")
LOGIN_USER = os.getenv("LOGIN_USER", "admin")
LOGIN_PASS = os.getenv("LOGIN_PASS", "admin123")
BANK_CHOICE = os.getenv("BANK_CHOICE", "prod") # prod | test | treinamento
SCREEN_DIR = os.getenv("SCREEN_DIR", "./screenshots")
OUTPUT_PPTX = os.getenv("OUTPUT_PPTX", "./demo_funcionalidades.pptx")
HEADLESS = os.getenv("AUTOCAPTURE_HEADLESS", "false").lower() == "true"
VIEWPORT_W = int(os.getenv("AUTOCAPTURE_VIEWPORT_W", "1440"))
VIEWPORT_H = int(os.getenv("AUTOCAPTURE_VIEWPORT_H", "900"))
# Importa seu mapa de módulos (aproveita rótulos e grupos)
try:
from modules_map import MODULES
except Exception:
MODULES = {}
print("⚠️ Não consegui importar modules_map.py. Ele deve estar no mesmo diretório do script.")
# PowerPoint
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
# Playwright
from playwright.sync_api import sync_playwright
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
def ensure_dir(path: str):
os.makedirs(path, exist_ok=True)
def sanitize(s: str) -> str:
"""Remove/normaliza caracteres inválidos de nomes (Windows-safe)."""
s = re.sub(r"[\\/:*?\"<>|]", "_", s) # remove proibidos
s = re.sub(r"\s+", "_", s.strip()) # espaços -> _
return s
def bank_label(choice: str) -> str:
return {
"prod": "Banco 1 (📗 Produção)",
"test": "Banco 2 (📕 Teste)",
"treinamento": "Banco 3 (📘 Treinamento)",
}.get(choice, choice)
def save_artifacts_on_fail(page, tag="fail"):
"""Salva HTML e screenshot quando algo dá errado."""
ensure_dir(SCREEN_DIR)
tag = sanitize(tag)
try:
html_path = os.path.join(SCREEN_DIR, f"{tag}_page.html")
img_path = os.path.join(SCREEN_DIR, f"{tag}_page.png")
with open(html_path, "w", encoding="utf-8") as f:
f.write(page.content())
page.screenshot(path=img_path, full_page=True)
print(f"📝 Artefatos salvos: {html_path}, {img_path}")
except Exception as e:
print(f"⚠️ Falha ao salvar artefatos de erro: {e}")
def select_by_label(page, select_label: str, option_text: str):
"""
Seleciona uma opção em um st.selectbox, procurando pelo label (texto visível).
• Varre todos os elementos com data-testid="stSelectbox"
• Encontra o que contém o label desejado (case-insensitive)
• Abre o combobox e clica na opção exata
"""
boxes = page.locator('[data-testid="stSelectbox"]')
count = boxes.count()
if count == 0:
raise RuntimeError("Nenhum stSelectbox encontrado na página.")
found = False
for i in range(count):
box = boxes.nth(i)
try:
txt = box.inner_text().strip()
except Exception:
continue
if select_label.lower() in txt.lower():
box.locator('div[role="combobox"]').first.click()
page.locator('div[role="listbox"]').get_by_text(option_text, exact=True).click()
found = True
break
if not found:
raise RuntimeError(f"Selectbox com label '{select_label}' não encontrado.")
def bypass_quiz(page):
"""
Tenta sair da tela de Quiz, caso esteja bloqueando a navegação.
Procura ações típicas: 'Voltar ao sistema', 'Finalizar', 'Continuar'.
"""
# 1) Voltar ao sistema
try:
if page.get_by_text("Voltar ao sistema").count() > 0:
page.get_by_text("Voltar ao sistema").click()
page.wait_for_timeout(600)
return
except Exception:
pass
# 2) Finalizar
try:
if page.get_by_role("button", name="Finalizar").count() > 0:
page.get_by_role("button", name="Finalizar").click()
page.wait_for_timeout(600)
return
except Exception:
pass
# 3) Continuar
try:
if page.get_by_role("button", name="Continuar").count() > 0:
page.get_by_role("button", name="Continuar").click()
page.wait_for_timeout(600)
return
except Exception:
pass
# 4) Se nada funcionar, salva artefatos para analisarmos o DOM real
save_artifacts_on_fail(page, "quiz_bypass")
def do_login(page):
page.goto(APP_URL, timeout=60000)
page.wait_for_load_state("networkidle")
page.wait_for_timeout(800)
# Seleciona Banco (selectbox "Usar banco:")
try:
select_by_label(page, "Usar banco:", bank_label(BANK_CHOICE))
except Exception as e:
print(f"⚠️ Falha ao selecionar banco: {e}")
save_artifacts_on_fail(page, "select_bank")
# Fallback por texto simples (última tentativa)
try:
page.get_by_text("Usar banco:").click()
page.get_by_text(bank_label(BANK_CHOICE), exact=True).click()
except Exception:
pass
# Preenche credenciais
try:
page.get_by_label("Usuário").fill(LOGIN_USER)
except Exception:
page.locator('label:has-text("Usuário")').locator("xpath=..").locator('input').fill(LOGIN_USER)
try:
page.get_by_label("Senha").fill(LOGIN_PASS)
except Exception:
page.locator('label:has-text("Senha")').locator("xpath=..").locator('input').fill(LOGIN_PASS)
# Entrar
try:
page.get_by_role("button", name="Entrar").click()
except Exception:
page.get_by_text("Entrar").click()
page.wait_for_load_state("networkidle")
page.wait_for_timeout(1000)
# Captura pós-login
page.screenshot(path=os.path.join(SCREEN_DIR, "00_pos_login.png"), full_page=True)
# Bypass do Quiz (se existir)
bypass_quiz(page)
# Captura pós-quiz
page.screenshot(path=os.path.join(SCREEN_DIR, "01_pos_quiz.png"), full_page=True)
def clear_search(page):
"""Limpa campo 'Pesquisar módulo:' para não filtrar nada (opcional)."""
try:
page.get_by_label("Pesquisar módulo:").fill("")
page.wait_for_timeout(200)
except Exception:
# Fallback: tenta input na sidebar
try:
sb = page.locator('[data-testid="stSidebar"]').first
sb.locator('input').first.fill("")
except Exception:
pass
def capture_all_screens():
ensure_dir(SCREEN_DIR)
screenshots = []
from playwright.sync_api import TimeoutError
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=HEADLESS)
context = browser.new_context(viewport={"width": VIEWPORT_W, "height": VIEWPORT_H})
page = context.new_page()
# Login
do_login(page)
# Grupos
grupos = sorted({MODULES[mid].get("grupo", "Outros") for mid in MODULES}) if MODULES else []
if not grupos:
print("⚠️ MODULES está vazio. Não há módulos para capturar.")
save_artifacts_on_fail(page, "no_modules")
context.close(); browser.close()
return screenshots
for grupo in grupos:
try:
clear_search(page)
select_by_label(page, "Selecione a operação:", grupo)
page.wait_for_timeout(500)
gshot = os.path.join(SCREEN_DIR, f"{BANK_CHOICE}_grupo_{sanitize(grupo)}.png")
page.screenshot(path=gshot, full_page=True)
print(f"📸 Grupo: {grupo}{gshot}")
except Exception as e:
print(f"⚠️ Falha ao selecionar grupo '{grupo}': {e}")
save_artifacts_on_fail(page, f"grupo_{grupo}")
continue
# Módulos do grupo
mod_ids = [mid for mid in MODULES if MODULES[mid].get("grupo", "Outros") == grupo]
for mid in mod_ids:
label = MODULES[mid].get("label", mid)
try:
select_by_label(page, "Selecione o módulo:", label)
page.wait_for_load_state("networkidle")
page.wait_for_timeout(800)
fname = f"{BANK_CHOICE}_{sanitize(grupo)}_{sanitize(mid)}.png"
fpath = os.path.join(SCREEN_DIR, fname)
page.screenshot(path=fpath, full_page=True)
screenshots.append((mid, label, grupo, fpath))
print(f"📸 Módulo: {label}{fpath}")
except Exception as e:
print(f"❌ Falha ao capturar módulo '{label}': {e}")
save_artifacts_on_fail(page, f"mod_{mid}")
traceback.print_exc()
continue
context.close()
browser.close()
return screenshots
def build_pptx(screens, out_path):
prs = Presentation()
# Slide de título
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "Apresentação do Sistema (ARM LoadApp)"
subtitle = slide.placeholders[1].text_frame
subtitle.clear()
p = subtitle.paragraphs[0]
p.text = f"Ambiente: {bank_label(BANK_CHOICE)} | Gerado em {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}"
p.font.size = Pt(14)
# Slides por módulo
for mid, label, grupo, fpath in screens:
layout = prs.slides.add_slide(prs.slide_layouts[5]) # Title Only
layout.shapes.title.text = f"{label}{grupo}"
left, top, width = Inches(0.5), Inches(1.2), Inches(9)
try:
layout.shapes.add_picture(fpath, left, top, width=width)
except Exception:
tx = layout.shapes.add_textbox(left, top, Inches(9), Inches(1))
tf = tx.text_frame
tf.text = f"(Falha ao inserir imagem: {os.path.basename(fpath)})"
tf.paragraphs[0].font.color.rgb = RGBColor(200, 0, 0)
prs.save(out_path)
print(f"🎉 PPTX gerado: {out_path}")
def main():
print(f"🚀 Captura em {APP_URL} | Banco: {BANK_CHOICE} ({bank_label(BANK_CHOICE)}) | headless={HEADLESS}")
ensure_dir(SCREEN_DIR)
screens = capture_all_screens()
if not screens:
print("⚠️ Nenhuma captura gerada. Veja os artefatos na pasta e revise seletores/menus.")
return
build_pptx(screens, OUTPUT_PPTX)
if __name__ == "__main__":
main()