Pular para conteúdo

Screenshots do manual (receita reproduzível)

Screenshot de manual não é print tirado à mão: esse apodrece na primeira mudança de UI e ninguém lembra como refazer. A regra é que screenshot é regenerável com 1 comando. O tooling de geração vive em scripts/screenshots/ (executável, portanto fora de docs/constituição §5); os PNGs gerados vão em docs/public/img/, versionados e referenciados pelo docs/public/manual/. Nomeie o PNG semanticamente quando há 1 imagem por tela (dre.png, visao-geral.png — comum em BI); use <slug>-NN.png quando há várias imagens na mesma página.

O como depende da stack — web renderizada no servidor e Flutter usam mecanismos diferentes.

Web renderizada no servidor (Micronaut/Java, HTML/Bootstrap)

Receita provada em produção. Três peças em scripts/screenshots/:

  1. seed.sh — sobe o app em modo dev e popula dados de exemplo. Nas telas que exigem login, use um bypass de auth só do modo dev (nunca em produção) para alcançar as views do manual.
  2. shots.yml — arquivo declarativo do shot-scraper (CLI sobre Playwright): cada item navega até uma URL, opcionalmente roda um JS na página e captura o PNG no caminho de docs/public/img/.
  3. Um comando que encadeia os dois, ex.: ./scripts/screenshots/seed.sh && shot-scraper multi scripts/screenshots/shots.yml.

Exemplo de shots.yml:

- output: docs/public/img/importar-frota-01.png
  url: http://localhost:8080/importacoes/frota
  width: 1280
- output: docs/public/img/importar-frota-02.png
  url: http://localhost:8080/importacoes/frota
  # abre o modal de forma síncrona (ver a dica do Bootstrap abaixo)
  javascript: |
    document.querySelector('#modalResumo').classList.remove('fade');
    bootstrap.Modal.getOrCreateInstance('#modalResumo').show();

Dica (Bootstrap): o shot-scraper fotografa antes da animação de transição, então o modal sai translúcido/deslocado. Remova a classe .fade do modal e abra-o pela API (bootstrap.Modal(...).show()) para uma captura síncrona e nítida.

Mudou a UI? Rode o comando de novo — os PNGs se atualizam e o PR mostra o diff visual.

Flutter

Não use shot-scraper — Flutter renderiza num canvas (Skia/CanvasKit), sem DOM para navegar ou selecionar. A captura é pelo próprio Flutter. Receita provada no app web bi-transporte (Vantroba): 9 telas do manual com 1 comando.

Alvo: build Linux desktop (-d linux), não web. Mesmo motor de render do web (CanvasKit/Skia) → telas idênticas, mas sem chromedriver (versão casada com o Chrome) nem a fragilidade do headless web.

Capturar com RepaintBoundary, não takeScreenshot. Envolva o app num RepaintBoundary(key: ...) e, depois do pumpAndSettle, faça boundary.toImage(pixelRatio: 1.5)toByteData(png) → grave o PNG em docs/public/img/ com dart:io. ⚠️ IntegrationTestWidgetsFlutterBinding.takeScreenshot não tem implementação no desktop Linux (MissingPluginException no canal plugins.flutter.io/integration_test; só Android/iOS/web). RepaintBoundary é Flutter puro e funciona.

"seed" = dentro do teste: injete um banco/datasource em memória semeado + sessão fake (SharedPreferences.setMockInitialValues). Com PowerSync/SQLite: um SQLite in-memory semeado com o DDL do schema PowerSync — os caches carregam dele e todos os painéis rendem com dados reais. Isso exige um seam de teste no bootstrap de DI (ex. um parâmetro opcional databaseOverride no configureDependencies); se outros singletons declaram dependsOn dele, registre o override como singleton async (registerSingletonAsync — o dependsOn:[AppDatabase] do get_it exige singleton async).

Flag kScreenshotMode (bool.fromEnvironment('SCREENSHOTS')) — o equivalente Flutter da "dica do .fade" do Bootstrap: (a) esconde overlays de dev, (b) troca a marca d'água pelo rótulo de teste, (c) não inicia Timers periódicos (senão o teardown acusa "timer still pending").

Dirija a UI por rota e serviço, não por tap onde der: navegue com appRouter.go('/painel') e mude estado pelo serviço (ex. filtros) — mais robusto que achar widget. Tabela longa: setSurfaceSize por shot (janela mais alta).

Estabilize: pumpAndSettle com timeout curto + fallback de pumps fixos (gráficos/animações perpétuas nunca "settam").

Gotcha do veredito: ao descartar a árvore no fim, o app emite ruído de dispose/timer sob o harness e o teste "falha" no teardown, depois de todas as capturas. O script decide sucesso pela existência dos PNGs esperados, não pelo exit code — || true no comando + checagem dos arquivos.

Comando (scripts/screenshots/gerar.sh):

fvm flutter drive --driver=test_driver/integration_test.dart \
  --target=integration_test/<suite>_test.dart -d linux \
  --dart-define=SCREENSHOTS=true --dart-define=SHOTS_DIR=$PWD/docs/public/img

Custo de adoção

  • Suporte linux/ no repo: flutter create --platforms=linux . (~13 arquivos CMake; versionar — é tooling de screenshot; o app deploya só como web).
  • Toolchain nativo (uma vez): cmake ninja-build clang pkg-config libgtk-3-dev libcurl4-openssl-dev libssl-dev zlib1g-dev lld (curl/ssl/zlib = sentry-native; lld = linker dos native assets powersync/sqlite).
  • Cache de CMake velho instala em /usr/local (Permission denied) → rm -rf build/linux.

Revisar o diff antes de commitar

Regenerar não é o fim — o PR só está pronto depois de abrir a imagem e confirmar o elemento novo (o botão, o rótulo, o layout que motivou a mudança). Rodar o gerador não prova nada: se o seed não alcançou o estado certo, o comando "passa" e grava a tela velha — a mesma falha do gate que passa sem tocar o runtime real (constituição §6).

Armadilha do diff de só-ruído: geradores com marca d'água ou elemento aleatório (timestamp, seed visível) produzem diff em todos os PNGs, mesmo nos que não mudaram de verdade. Não commite a enxurrada: reverta os PNGs de só-ruído (git checkout -- <png>) e mantenha só o(s) que teve(tiveram) mudança visual real. O kScreenshotMode (Flutter, acima) já troca a marca d'água pelo rótulo de teste justamente para reduzir esse ruído; quando o gerador não tem esse modo, a revisão manual do diff é o que separa sinal de ruído.

Referência viva: bi-transportelib/core/config/screenshot_mode.dart, lib/locator.dart (seam databaseOverride), integration_test/manual_screenshots_test.dart + _fixtures.dart, test_driver/integration_test.dart, scripts/screenshots/gerar.sh, docs/operacao/gerar-screenshots-do-manual.md.