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/:
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.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 dedocs/public/img/.- 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-transporte—lib/core/config/screenshot_mode.dart,lib/locator.dart(seamdatabaseOverride),integration_test/manual_screenshots_test.dart+_fixtures.dart,test_driver/integration_test.dart,scripts/screenshots/gerar.sh,docs/operacao/gerar-screenshots-do-manual.md.