| |
|
| |
|
| | """
|
| | 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
|
| |
|
| |
|
| | 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")
|
| |
|
| | 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"))
|
| |
|
| |
|
| | 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.")
|
| |
|
| |
|
| | from pptx import Presentation
|
| | from pptx.util import Inches, Pt
|
| | from pptx.dml.color import RGBColor
|
| |
|
| |
|
| | from playwright.sync_api import sync_playwright
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | 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)
|
| | s = re.sub(r"\s+", "_", s.strip())
|
| | 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'.
|
| | """
|
| |
|
| | 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
|
| |
|
| |
|
| | 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
|
| |
|
| |
|
| | 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
|
| |
|
| |
|
| | 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)
|
| |
|
| |
|
| | 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")
|
| |
|
| | try:
|
| | page.get_by_text("Usar banco:").click()
|
| | page.get_by_text(bank_label(BANK_CHOICE), exact=True).click()
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| | 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)
|
| |
|
| |
|
| | 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)
|
| |
|
| |
|
| | page.screenshot(path=os.path.join(SCREEN_DIR, "00_pos_login.png"), full_page=True)
|
| |
|
| |
|
| | bypass_quiz(page)
|
| |
|
| |
|
| | 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:
|
| |
|
| | 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()
|
| |
|
| |
|
| | do_login(page)
|
| |
|
| |
|
| | 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
|
| |
|
| |
|
| | 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 = 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)
|
| |
|
| |
|
| | for mid, label, grupo, fpath in screens:
|
| | layout = prs.slides.add_slide(prs.slide_layouts[5])
|
| | 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()
|
| |
|
| |
|