|
|
|
|
| """
|
| 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()
|
|
|
|
|