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