feat(debug): full UI screenshot sweep — captures modals/dialogs/flows/states

The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG
OPTIONS) that also drives every normally-hidden surface into view and captures
it under every skin, so all the UI a normal sweep misses can be reviewed at
once.

- The capture unit becomes a "surface" (SweepTarget: a base tab, optionally
  with a setup()/teardown() that forces a modal / multi-step flow / state
  overlay on top). Tabs are surfaces with a null setup. The existing tab sweep
  is folded into the same state machine (one code path). All sweep logic +
  the catalog + demo data move into a new src/app_sweep.cpp.
- Runs OFFLINE against injected demo data (installDemoWalletData snapshots then
  restores the real state_ — WalletState isn't copy-assignable, so a targeted
  field snapshot is used) and fires NO live op: capture_mode_ guards the
  seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs
  are neutralized by pre-setting their state (fetch_started_ + demo phrase;
  seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls.
- Catalog (v1): tabs + the bool-flag modals (import/export key, backup,
  seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps
  + lock/warmup/not-ready overlays + the send-confirm popup (via a new
  ui::SweepShowSendConfirm debug hook, since its state is send_tab-static).
- Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs
  each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs
  before the first-run-wizard early-return in render() so the sweep advances
  while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't
  throttled to the compositor's unfocused rate. Output:
  <config>/screenshots-full/<surface>/<skin>.png + index.md.
- DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification).

Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720,
[Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for
static skins on data-free surfaces — the rest vary by animated theme effects /
live price / relative timestamps (expected for a review tool, not a bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:57:54 -05:00
parent 5228da707f
commit a86cb8f0f3
11 changed files with 500 additions and 112 deletions

View File

@@ -1754,6 +1754,16 @@ int main(int argc, char* argv[])
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
}
// Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
// heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
{
static bool sweepVsyncOff = false;
const bool sweeping = app.isScreenshotSweeping();
if (sweeping != sweepVsyncOff) {
SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
sweepVsyncOff = sweeping;
}
}
SDL_GL_SwapWindow(window);
#endif
PERF_END("Present", _perfPresent);
@@ -1761,6 +1771,21 @@ int main(int argc, char* argv[])
// --- PerfLog: end frame ---
dragonx::util::PerfLog::instance().endFrame();
// Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
// settled, then quits when it finishes. Used to run the sweep without clicking the button.
{
static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
static int autoFrames = 0;
static bool autoStarted = false;
if (autoSweep) {
if (!autoStarted) {
if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; }
} else if (!app.isScreenshotSweeping()) {
running = false; // sweep complete
}
}
}
// Exit when shutdown is complete
if (app.shouldQuit()) {
running = false;