# -*- 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()