From ec7a1b07879c35a781a41735b97b46701df54f2c Mon Sep 17 00:00:00 2001 From: Istvan Matejcsok <119620946+matejcsok-ee@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:54:20 +0200 Subject: [PATCH] screenshots: R2-hosted manifest becomes the baseline source of truth (morelli cutover) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed tests/screenshot-manifest.json is retired. CI now downloads baselines/pcbjam/manifest.json (manifest v3, written only by the morelli review app + its seed script) to the gitignored .baseline-manifest.json, and everything downstream (pull, verify, compare) reads that copy: - config.ts: MANIFEST_VERSION 3, MANIFEST_PATH .baseline-manifest.json, R2_BASELINES_MANIFEST_KEY; ManifestEntry grows opaque provenance - r2-sync.ts: new --manifest mode (atomic fetch; no-creds skip DELETES a stale copy so the gate skips rather than using old baselines); --push gone (bytes enter the CAS only via morelli's promote) - compare.ts: hard-skips when no manifest was fetched — a stale warm cache can never gate - wasm-build.yml: fetch-manifest step before the baselines cache; cache key now hashes the fetched manifest; the gen-manifest --check lint gate goes with the committed manifest - deleted: screenshot-manifest.json, promote.ts, changelog.ts, gen-manifest.ts, screenshot-changelog.yml, promote-screenshots skill - docs (CLAUDE/README/TESTING/WHATWORKS/tools README): promote flow is now https://pcbjam-morelli-staging.pcbjam-staging.workers.dev Validated locally against the real bucket: fetch-manifest (492), cold pull 492 / warm pull cached=492, no-creds skip chain, compare gate skip. Co-Authored-By: Claude Fable 5 --- .claude/skills/promote-screenshots/SKILL.md | 42 - .github/workflows/screenshot-changelog.yml | 45 - .github/workflows/wasm-build.yml | 50 +- CLAUDE.md | 4 +- README.md | 15 +- tests/.gitignore | 9 +- tests/README.md | 10 +- tests/TESTING.md | 21 +- tests/WHATWORKS.md | 18 +- tests/package.json | 5 +- tests/screenshot-manifest.json | 3946 ------------------- tests/tools/screenshots/README.md | 72 +- tests/tools/screenshots/changelog.ts | 212 - tests/tools/screenshots/compare.ts | 13 +- tests/tools/screenshots/config.ts | 43 +- tests/tools/screenshots/gen-manifest.ts | 191 - tests/tools/screenshots/post-discord.ts | 2 +- tests/tools/screenshots/promote.ts | 205 - tests/tools/screenshots/r2-store.ts | 15 + tests/tools/screenshots/r2-sync.ts | 114 +- 20 files changed, 239 insertions(+), 4793 deletions(-) delete mode 100644 .claude/skills/promote-screenshots/SKILL.md delete mode 100644 .github/workflows/screenshot-changelog.yml delete mode 100644 tests/screenshot-manifest.json delete mode 100644 tests/tools/screenshots/changelog.ts delete mode 100644 tests/tools/screenshots/gen-manifest.ts delete mode 100644 tests/tools/screenshots/promote.ts diff --git a/.claude/skills/promote-screenshots/SKILL.md b/.claude/skills/promote-screenshots/SKILL.md deleted file mode 100644 index 43a16b9..0000000 --- a/.claude/skills/promote-screenshots/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: promote-screenshots -description: Promote a CI run's screenshot renders as the new baselines in the R2 bucket (pcbjam-ci-screenshots). Churn-free - only meaningfully-changed images upload; git gets only the tests/screenshot-manifest.json diff, never PNGs. Needs the read-write R2 keypair in tests/.env. Usage - "/promote-screenshots [--prune]". (scoped to pcbjam/ - covers the KiCad WASM e2e pipeline in tests/) ---- - -# promote-screenshots (pcbjam) - -Bless a CI run's rendered screenshots as the new baselines. Baselines live in -the private R2 bucket `pcbjam-ci-screenshots` (prod Cloudflare account), -content-addressed as `sha256/.png`; the committed -`tests/screenshot-manifest.json` pins each `/` to a hash. **Only -the manifest diff lands in git — never PNGs.** - -## Prerequisites - -- The READ-WRITE R2 keypair in the gitignored `tests/.env` (auto-loaded by the - tooling, shell env wins; format in `tests/tools/screenshots/README.md`, - values from the team vault). **Never print, echo, or commit these values.** - If `.env` is missing, ask the user to fill it — promote fails fast without it. -- The CI run id: `gh run list --workflow ci-ubicloud.yml` on PCBJam/pcbjam - (mind the active `gh` account — PCBJam repos need `matejcsok-pcb`). - -## Steps - -1. From `tests/`, dry-run first and show the user the plan: - `npm run screenshots:promote -- --run --dry-run` -2. Sanity-check it: a handful of UPDATE/ADD lines for an intentional UI change - is normal; hundreds of UPDATEs means environment drift — stop and confirm - with the user before applying. -3. Apply: same command without `--dry-run`. Add `--prune` only when the user - confirms screenshots were intentionally removed (prune edits the manifest; - R2 objects are never deleted — old commits still resolve). -4. `git status` must show ONLY `tests/screenshot-manifest.json` modified. - Commit that diff; on main it triggers the Discord baseline changelog. - -## Never - -- Never commit files under `tests/baseline-screenshots/` (the CI manifest - check fails the build if you do) or the `.env`. -- Never promote local (Mac) renders via `--from` — CI's Linux render is the - only source of truth. -- Never hand-edit hashes in the manifest; promote regenerates it. diff --git a/.github/workflows/screenshot-changelog.yml b/.github/workflows/screenshot-changelog.yml deleted file mode 100644 index b93c784..0000000 --- a/.github/workflows/screenshot-changelog.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: screenshot-changelog - -# Discord trigger B: when a push to main changes the screenshot manifest (the -# committed pin of every R2-stored baseline), post an old | new+boxes | heatmap -# triptych per changed baseline (ADDED image / REMOVED title too). No build, no -# GPU — it diffs two git revisions of the manifest and fetches the PNG bytes -# from the R2 CAS bucket (immutable objects, so the old rev's hashes always -# resolve), so it runs in ~30s. Complements the re-render drift gate in -# wasm-build.yml (which catches un-blessed renders); this is the human-facing -# feed of intentional baseline updates as they land. - -on: - push: - branches: [main] - paths: - - 'tests/screenshot-manifest.json' - -concurrency: - group: screenshot-changelog-${{ github.ref }} - cancel-in-progress: false - -jobs: - changelog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - # HEAD^ is needed to diff the tip commit's manifest changes. (A push of - # multiple commits reports only the tip commit's baseline diff — fine for - # a changelog; baseline refreshes are single commits in practice.) - fetch-depth: 2 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install screenshot tooling deps - working-directory: tests - run: npm ci - - name: Post baseline changelog to Discord - working-directory: tests - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - CI_SCREENSHOTS_S3_ENDPOINT: ${{ vars.CI_SCREENSHOTS_S3_ENDPOINT }} - CI_SCREENSHOTS_S3_ACCESS_KEY_ID: ${{ secrets.CI_SCREENSHOTS_S3_ACCESS_KEY_ID }} - CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY: ${{ secrets.CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY }} - run: npm run screenshots:changelog diff --git a/.github/workflows/wasm-build.yml b/.github/workflows/wasm-build.yml index 290aa22..d1a84c4 100644 --- a/.github/workflows/wasm-build.yml +++ b/.github/workflows/wasm-build.yml @@ -318,19 +318,37 @@ jobs: working-directory: tests run: npm ci - # Baseline screenshots live in a private R2 bucket, pinned by the committed - # manifest; this cache keyed on the manifest hash makes most fetches a - # no-op. restore-keys makes a manifest change INCREMENTAL (restore the - # previous tree, download only the changed objects) instead of a full - # ~35MB re-download. A stale or partial restore is safe: r2-sync - # hash-verifies every file and deletes unlisted ones, and compare.ts - # refuses to gate against an incomplete cache. + # The baseline manifest lives in R2 (written only by the morelli review + # app + its seed script) — download it FIRST so this whole run pins to + # one manifest version and the cache step below can key on its hash. + # continue-on-error + the no-creds skip (which also deletes any stale + # local copy) keep secretless callers green — every later screenshot + # step then skips rather than gating on outdated baselines. + - name: Fetch baseline manifest from R2 + if: inputs.run_tests + continue-on-error: true + working-directory: tests + env: + CI_SCREENSHOTS_S3_ENDPOINT: ${{ vars.CI_SCREENSHOTS_S3_ENDPOINT }} + CI_SCREENSHOTS_S3_ACCESS_KEY_ID: ${{ secrets.CI_SCREENSHOTS_S3_ACCESS_KEY_ID }} + CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY: ${{ secrets.CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY }} + run: npm run screenshots:fetch-manifest + + # Baseline screenshots live in a private R2 bucket, pinned by the manifest + # fetched above; this cache keyed on the manifest hash makes most fetches + # a no-op (hashFiles evaluates at step run time, AFTER the fetch; a + # skipped fetch hashes to empty → restore-keys still warms the tree). + # restore-keys makes a manifest change INCREMENTAL (restore the previous + # tree, download only the changed objects) instead of a full ~35MB + # re-download. A stale or partial restore is safe: r2-sync hash-verifies + # every file and deletes unlisted ones, and compare.ts refuses to gate + # against an incomplete cache. - name: Cache screenshot baselines if: inputs.run_tests uses: actions/cache@v4 with: path: tests/baseline-screenshots - key: baselines-${{ hashFiles('tests/screenshot-manifest.json') }} + key: baselines-${{ hashFiles('tests/.baseline-manifest.json') }} restore-keys: | baselines- @@ -349,19 +367,17 @@ jobs: run: npm run screenshots:fetch # Cheap hygiene gates (no build needed): the determinism lint keeps the - # banned flake patterns out of the specs, the manifest check validates - # screenshot-manifest.json (v2 schema, sorted/unique hashes) and fails if - # baseline PNGs are ever re-committed to git (credential-free — it never - # touches R2, so it gates secretless callers identically), and - # the CI-coverage lint proves every spec file on disk is reachable from - # the npm scripts THIS workflow invokes (a spec/project that CI never - # runs is how the web suite once rotted unnoticed). - - name: Lint test determinism + screenshot manifest + CI coverage + # banned flake patterns out of the specs, and the CI-coverage lint proves + # every spec file on disk is reachable from the npm scripts THIS workflow + # invokes (a spec/project that CI never runs is how the web suite once + # rotted unnoticed). (The old screenshot-manifest --check gate went with + # the committed manifest — the manifest now lives in R2, written only by + # the morelli app, which validates on every promote.) + - name: Lint test determinism + CI coverage if: inputs.run_tests working-directory: tests run: | npm run lint:determinism - npm run screenshots:manifest -- --check npm run lint:ci-coverage - name: Install web workspace deps (collab bundle) diff --git a/CLAUDE.md b/CLAUDE.md index 38da11d..2a0f956 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,8 @@ The e2e tests are in /tests, with a README and WHATWORKS md files Test determinism rules (no blind sleeps/ifs, `stableShot` screenshots, retries:0) are in tests/TESTING.md, enforced by `npm run lint:determinism`. The e2e tests are separated per feature Wxwidgets wasm port has hooks for finding positions of UI elements, tests use that -The test screenshot baselines live in a private R2 bucket (content-addressed by sha256), pinned per engine by the committed tests/screenshot-manifest.json; tests/baseline-screenshots/{chromium,firefox}/ is a gitignored local cache — `cd tests && npm run screenshots:fetch` materializes it (needs the R2 credentials in tests/tools/screenshots/README.md). CI's Linux render is the source of truth (tooling: tests/tools/screenshots/, see its README). -To update baselines, promote a CI run's render (churn-free — only meaningfully-changed images re-upload): `cd tests && npm run screenshots:promote -- --run ` (needs the read-write R2 credentials), then commit the manifest diff — never commit PNGs. `npm run screenshots:check` is the local gate (fetch first); on each main push CI posts a screenshot-diff + runtime-perf report to Discord. +The test screenshot baselines live in a private R2 bucket (content-addressed by sha256), pinned per engine by the R2-hosted manifest baselines/pcbjam/manifest.json — NOTHING manifest-related is in git; tests/baseline-screenshots/{chromium,firefox}/ is a gitignored local cache — `cd tests && npm run screenshots:fetch-manifest && npm run screenshots:fetch` materializes it (needs the R2 credentials in tests/tools/screenshots/README.md). CI's Linux render is the source of truth (tooling: tests/tools/screenshots/, see its README). +To update baselines, promote a CI run's screenshots in the morelli review app (https://pcbjam-morelli-staging.pcbjam-staging.workers.dev — pick the run, review the diffs, bulk-select, Promote). CI uploads every run's renders to R2 (runs/pcbjam//, 30-day retention) for that purpose. No git commit is involved. `npm run screenshots:check` is the local gate (fetch-manifest + fetch first); on each main push CI posts a screenshot-diff + runtime-perf report to Discord. The tests have log files in tests/logs/{wxwidgets/kicad}/{test-name} after each run where the js console and cpp logs are visible Always check screenshots for validating tests Run e2e tests from /tests folder: `npm run test:e2e` (full CI project set, one merged playwright.config.ts) or `npm run test:kicad` (firefox shortcut) — not playwright directly. One spec/engine: `npx playwright test --project=kicad-firefox kicad/pcbnew.spec.ts`. Web-app suite: `npm run test:web`. diff --git a/README.md b/README.md index 86b5abd..6fac14f 100644 --- a/README.md +++ b/README.md @@ -195,15 +195,18 @@ See [tests/README.md](tests/README.md) for test documentation. ### Screenshots CI's Linux render is the source of truth for baseline screenshots. On each `main` -push, CI compares its render against the committed baselines and posts the diff -(plus the runtime-perf numbers) to Discord. To update baselines after an intended -render change, promote a CI run's render — only meaningfully-changed images -restage, so it stays churn-free: +push, CI compares its render against the baselines (pinned by the R2-hosted +manifest — nothing screenshot-related is committed) and posts the diff (plus the +runtime-perf numbers) to Discord. To update baselines after an intended render +change, promote the CI run's screenshots in the morelli review app +(https://pcbjam-morelli-staging.pcbjam-staging.workers.dev) — pick the run, +review the diffs, bulk-select, Promote. CI uploads every run's renders to R2 +for that purpose (30-day retention). ```bash cd tests -npm run screenshots:check # local gate: current vs baselines -npm run screenshots:promote -- --run # adopt a CI run's render, then commit +npm run screenshots:fetch-manifest && npm run screenshots:fetch # materialize baselines +npm run screenshots:check # local gate: current vs baselines ``` See [tests/tools/screenshots/README.md](tests/tools/screenshots/README.md). diff --git a/tests/.gitignore b/tests/.gitignore index c96ffd2..633c237 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,8 +1,11 @@ # Screenshot model: specs capture via stableShot() → page.screenshot into test-results/, which is # transient and gitignored at the repo root (/tests/test-results/). Baselines live in a private R2 -# bucket, content-addressed by sha256 and pinned by the committed screenshot-manifest.json; -# tests/baseline-screenshots/ is a gitignored local cache (`npm run screenshots:fetch`), authored -# from CI's deterministic Linux render and diffed OFFLINE by tools/screenshots. (3d-regression/ and +# bucket, content-addressed by sha256 and pinned by the R2-hosted manifest (fetched to the +# gitignored .baseline-manifest.json below — NOTHING manifest-related is committed); +# tests/baseline-screenshots/ is a gitignored local cache (`npm run screenshots:fetch-manifest && +# npm run screenshots:fetch`), authored from CI's deterministic Linux render and diffed OFFLINE by +# tools/screenshots. Baselines are promoted from a CI run in the morelli app. (3d-regression/ and # gal-regression/ baselines are still committed.) There are no Playwright-native *-snapshots/ # baselines — Playwright does no inline screenshot comparison. +.baseline-manifest.json logs/ diff --git a/tests/README.md b/tests/README.md index fc1cb0c..63dceed 100644 --- a/tests/README.md +++ b/tests/README.md @@ -55,7 +55,7 @@ tests/ ├── logs/ # Test logs (auto-generated) ├── test-results/{chromium,firefox}/ # Screenshots per engine (auto-generated) ├── baseline-screenshots/{chromium,firefox}/ # Reference screenshots per engine — gitignored cache of the R2 bucket (npm run screenshots:fetch) -├── screenshot-manifest.json # Committed pin of every baseline: {name, engine, sha256, …} (regenerated by screenshots:promote) +├── .baseline-manifest.json # Gitignored copy of the R2-hosted baseline manifest (npm run screenshots:fetch-manifest) ├── apps/ # Built WASM test applications │ ├── minimal_test.html # Main test app │ └── standalone/ # Individual component test apps @@ -83,13 +83,15 @@ Example log format: ## Screenshots Tests capture raw PNGs to `test-results//` (engine-scoped via -`stableShot`/`shotPath`). The offline gate compares them against the committed -per-engine baselines: +`stableShot`/`shotPath`). The offline gate compares them against the per-engine +baselines pinned by the R2-hosted manifest: ```bash +npm run screenshots:fetch-manifest && npm run screenshots:fetch npm run screenshots:check ``` CI's Linux render is the source of truth — update baselines by promoting a CI -run (`npm run screenshots:promote -- --run `), never by copying +run in the morelli review app +(https://pcbjam-morelli-staging.pcbjam-staging.workers.dev), never by copying local renders. Rules and details: `TESTING.md`. ## Viewing the App Directly diff --git a/tests/TESTING.md b/tests/TESTING.md index fb504e7..3dc3674 100644 --- a/tests/TESTING.md +++ b/tests/TESTING.md @@ -46,15 +46,18 @@ only) — not playwright directly. One spec on one engine: `tests/baseline-screenshots//` (+ the still-committed `3d-regression/`, `gal-regression/`). - **Baselines live in the private R2 bucket `pcbjam-ci-screenshots`, not git**: - the committed `tests/screenshot-manifest.json` pins each `{name, engine}` to a - sha256, and `baseline-screenshots/` is a gitignored cache — materialize it with - `npm run screenshots:fetch` (needs the R2 credentials; see - `tools/screenshots/README.md`). CI's lint step validates the manifest and fails - if baseline PNGs are ever committed again. -- **CI's Linux render is the source of truth**; baselines are promoted from CI - (`npm run screenshots:promote -- --run `, needs the read-write - credentials) — commit only the regenerated manifest diff, never PNGs. A local - (Mac) check shows font/render noise and is not the gate. + the R2-hosted manifest `baselines/pcbjam/manifest.json` pins each + `{name, engine}` to a sha256, and `baseline-screenshots/` + + `.baseline-manifest.json` are gitignored caches — materialize them with + `npm run screenshots:fetch-manifest && npm run screenshots:fetch` (needs the + R2 credentials; see `tools/screenshots/README.md`). Nothing + screenshot-related is committed to git. +- **CI's Linux render is the source of truth**; baselines are promoted from a + CI run in the morelli review app + (https://pcbjam-morelli-staging.pcbjam-staging.workers.dev) — CI uploads each + run's renders to R2 (30-day retention), morelli shows the diffs and writes + the manifest on Promote. A local (Mac) check shows font/render noise and is + not the gate. - A continuously-animating state (timer, mid-slide) can't be a stable baseline — drop the shot. ## Retries diff --git a/tests/WHATWORKS.md b/tests/WHATWORKS.md index 3e71be1..94c97e7 100644 --- a/tests/WHATWORKS.md +++ b/tests/WHATWORKS.md @@ -405,7 +405,7 @@ tests/ ├── test-results/ │ ├── chromium/ # Latest run's captures per engine │ └── firefox/ -└── screenshot-manifest.json # Committed pin: {name, engine, sha256, …} per baseline +└── .baseline-manifest.json # GITIGNORED copy of the R2-hosted manifest (npm run screenshots:fetch-manifest) ``` Specs write via `stableShot(page, 'name.png')` / `shotPath(page, 'name.png')` @@ -416,7 +416,8 @@ browser, so the same spec on two engines produces two independent captures. ```bash cd tests -npm run screenshots:fetch # materialize the baseline cache from R2 (idempotent) +npm run screenshots:fetch-manifest # download the R2-hosted baseline manifest +npm run screenshots:fetch # materialize the baseline cache from R2 (idempotent) npm run screenshots:check ``` @@ -428,14 +429,13 @@ detection is driven by the manifest). On each main push, CI posts the same repor ### Updating Baseline Screenshots CI's Linux render is the source of truth — never copy local (Mac) renders into -the baselines. Promote from a CI run instead (churn-free: only meaningfully -changed images restage, and the manifest regenerates automatically): +the baselines. Promote from a CI run in the morelli review app instead +(churn-free: identical images keep their provenance, and the R2-hosted manifest +updates atomically — no git commit involved): -```bash -cd tests -npm run screenshots:promote -- --run # needs the read-write R2 keypair (tests/.env) -git commit # only screenshot-manifest.json changes — PNGs are uploaded to R2, never committed -``` +1. Open https://pcbjam-morelli-staging.pcbjam-staging.workers.dev (GitHub sign-in). +2. Pipeline `pcbjam` → pick the run → review the side-by-side/pixel diffs. +3. Select the intended changes (or "Select all changed + added") → Promote. ### Running Tests with Screenshots diff --git a/tests/package.json b/tests/package.json index 7144191..6d0423c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -18,14 +18,11 @@ "lint:determinism": "tsx tools/lint-determinism.ts", "lint:ci-coverage": "tsx tools/lint-ci-coverage.ts", "screenshots:check": "tsx tools/screenshots/compare.ts", - "screenshots:promote": "tsx tools/screenshots/promote.ts", "screenshots:noise": "tsx tools/screenshots/noise.ts", "screenshots:report": "tsx tools/screenshots/post-discord.ts", - "screenshots:changelog": "tsx tools/screenshots/changelog.ts", - "screenshots:manifest": "tsx tools/screenshots/gen-manifest.ts", + "screenshots:fetch-manifest": "tsx tools/screenshots/r2-sync.ts --manifest", "screenshots:fetch": "tsx tools/screenshots/r2-sync.ts --pull", "screenshots:upload-run": "tsx tools/screenshots/upload-run.ts", - "screenshots:push": "tsx tools/screenshots/r2-sync.ts --push", "3d:compare": "tsx tools/screenshots/compare-dirs.ts", "3d:check": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/native --out 3d-regression/output/diff/native-self --floors 3d-regression/floors.json --level native-self --label 3d-native --fail-on-change", "3d:check:webgl": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline-webgl --new 3d-regression/output/webgl --out 3d-regression/output/diff/webgl-self --floors 3d-regression/floors.json --level webgl-self --label 3d-webgl --fail-on-change", diff --git a/tests/screenshot-manifest.json b/tests/screenshot-manifest.json deleted file mode 100644 index f8097e2..0000000 --- a/tests/screenshot-manifest.json +++ /dev/null @@ -1,3946 +0,0 @@ -{ - "_note": "Baselines live in R2, content-addressed by sha256 — the local baseline-screenshots/ tree is a cache (`npm run screenshots:fetch`). Regenerated by `npm run screenshots:promote`; never edit hashes by hand. The name list is authoritative for removed-screenshot detection.", - "version": 2, - "storage": { - "bucket": "pcbjam-ci-screenshots", - "keyPrefix": "sha256/" - }, - "screenshots": [ - { - "name": "01-loading.png", - "engine": "chromium", - "sha256": "a8908f64d24597fc9af19df4bff8d91aac0b6a584a546698e1af73e9990b0c3d", - "bytes": 6017, - "width": 1280, - "height": 720 - }, - { - "name": "03-after-load.png", - "engine": "chromium", - "sha256": "ad71660243c16719d9a35c79e6393896f12fd8481b386fffec690238a06b1c7d", - "bytes": 28969, - "width": 1280, - "height": 720 - }, - { - "name": "04-controls-tab.png", - "engine": "chromium", - "sha256": "47edc72ce3c4a919f68a7a874c0b98928991cf4065d33f8f2ae235601337d80d", - "bytes": 38561, - "width": 1280, - "height": 720 - }, - { - "name": "05-text-input-tab.png", - "engine": "chromium", - "sha256": "131465acd65d48a7f848b7ee7e43b2afd9c140ea521e52257a3e52e0b94a3856", - "bytes": 32894, - "width": 1280, - "height": 720 - }, - { - "name": "06-text-input-typed.png", - "engine": "chromium", - "sha256": "fd1b96aabc2e65fd832221b59b1af309aefd23ae7ad4fd34a649c6c4ec6c5d73", - "bytes": 35416, - "width": 1280, - "height": 720 - }, - { - "name": "07-drawing-tab.png", - "engine": "chromium", - "sha256": "44287d1340094654bd1d7c643edf0b90e709923d15b5cd18c5898647ee286519", - "bytes": 37636, - "width": 1280, - "height": 720 - }, - { - "name": "08-drawing-done.png", - "engine": "chromium", - "sha256": "36dd1ec39ae8a1d04ef3db7be866d1f1d190c1749af2e32e73caa0473be0fb54", - "bytes": 37547, - "width": 1280, - "height": 720 - }, - { - "name": "09-lists-tab.png", - "engine": "chromium", - "sha256": "42be94987a02de93737ac151b787448e051332a9c8bb47ace48346a6d7e6a0fb", - "bytes": 42424, - "width": 1280, - "height": 720 - }, - { - "name": "10-lists-clicked.png", - "engine": "chromium", - "sha256": "42be94987a02de93737ac151b787448e051332a9c8bb47ace48346a6d7e6a0fb", - "bytes": 42424, - "width": 1280, - "height": 720 - }, - { - "name": "10b-choice-selected.png", - "engine": "chromium", - "sha256": "9086cf1da7128b832d68c2a2108264de44b74d7b85e79baa855f647c49a9ddfe", - "bytes": 42806, - "width": 1280, - "height": 720 - }, - { - "name": "11-file-menu.png", - "engine": "chromium", - "sha256": "7bc0bae8efd36bb3b20ee652adf1e8c851df67b3d7a695da64c8f9b8b3135dd6", - "bytes": 42434, - "width": 1280, - "height": 720 - }, - { - "name": "12-help-menu.png", - "engine": "chromium", - "sha256": "9086cf1da7128b832d68c2a2108264de44b74d7b85e79baa855f647c49a9ddfe", - "bytes": 42806, - "width": 1280, - "height": 720 - }, - { - "name": "13-final.png", - "engine": "chromium", - "sha256": "464f2025a79276fd74bbaf450f8b5f81ae00400d901f3940f2db232acbb33d08", - "bytes": 43092, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-00-board-loaded.png", - "engine": "chromium", - "sha256": "2bb785970e78ac20dafe353a4c628161d6406f517045f9e7674bed75a466dca8", - "bytes": 252106, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-models-pic_programmer-render.png", - "engine": "chromium", - "sha256": "e4e9a46c2a1f0360e9d36bccfdd4fe68550fa5673ce0012ed05c613df7a20d9f", - "bytes": 72189, - "width": 1084, - "height": 623 - }, - { - "name": "3d-viewer-models-pic_programmer.png", - "engine": "chromium", - "sha256": "251d1161cd3db748047df90c2bb434e91eb66cd68765183af35d7111b5a4ce36", - "bytes": 115779, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-pic_programmer-render.png", - "engine": "chromium", - "sha256": "c99b4d6daf7f030d7d87ef004b3e19f13c2b52aaa1f1d32ddfe91e73915f0766", - "bytes": 73632, - "width": 1084, - "height": 623 - }, - { - "name": "3d-viewer-pic_programmer.png", - "engine": "chromium", - "sha256": "7609604bac2b08f19bbba6f0ac162a17f537acb1c550eac04fa8d55210d15045", - "bytes": 115635, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-titlebar.png", - "engine": "chromium", - "sha256": "f472573ddb17ec190d80e7e13eb07e052faed7ef2c665ac2c13832c71795d810", - "bytes": 115497, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-00-layers.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-01-objects.png", - "engine": "chromium", - "sha256": "64fc1685bba9b1c987fef78871498b3206ed709bb8a1991bc2c7a8576faf100b", - "bytes": 104485, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-02-nets.png", - "engine": "chromium", - "sha256": "6867a91a6637a865457a6b370761792ac7c53267ffc6f048be5579ab43e7c66f", - "bytes": 84489, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-03-layers-again.png", - "engine": "chromium", - "sha256": "601393abb3624741be75e36723abcd55fdd752767009bed2e278a6a294149f5e", - "bytes": 105568, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-10-layers-scrolled.png", - "engine": "chromium", - "sha256": "b6e6740f8b9a7cd6de6f4827c79b4b6d23d7325cf11a7d28dbd7b53e14d84824", - "bytes": 105299, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-11-layers-scrolled-back.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-20-objects-scrolled.png", - "engine": "chromium", - "sha256": "77848309996306c017c4bd727e4d565912f2a41d71c4423e2f58560789959983", - "bytes": 108841, - "width": 1280, - "height": 720 - }, - { - "name": "aui-01-loaded.png", - "engine": "chromium", - "sha256": "dafef7fbc0cdcd31bdc386e9128856b3cd57bf526870137f2dca7666cfbd4b30", - "bytes": 28703, - "width": 1280, - "height": 720 - }, - { - "name": "aui-02-panels.png", - "engine": "chromium", - "sha256": "dafef7fbc0cdcd31bdc386e9128856b3cd57bf526870137f2dca7666cfbd4b30", - "bytes": 28703, - "width": 1280, - "height": 720 - }, - { - "name": "aui-03-close-clicked.png", - "engine": "chromium", - "sha256": "dafef7fbc0cdcd31bdc386e9128856b3cd57bf526870137f2dca7666cfbd4b30", - "bytes": 28703, - "width": 1280, - "height": 720 - }, - { - "name": "aui-04-dragged.png", - "engine": "chromium", - "sha256": "0f08df98b9a49f9015f69db017642a8805b4fc095848a4f5e97e92bce033441f", - "bytes": 28711, - "width": 1280, - "height": 720 - }, - { - "name": "aui-05-multi-panel.png", - "engine": "chromium", - "sha256": "685f9930941f2db9ddf75c2ab0c93199196754cdc7c6efd3d8ef7c69e9720c56", - "bytes": 28837, - "width": 1280, - "height": 720 - }, - { - "name": "aui-resize-01-hover.png", - "engine": "chromium", - "sha256": "dafef7fbc0cdcd31bdc386e9128856b3cd57bf526870137f2dca7666cfbd4b30", - "bytes": 28703, - "width": 1280, - "height": 720 - }, - { - "name": "aui-resize-02-mid-drag-live.png", - "engine": "chromium", - "sha256": "1f440434987e994552bb9280b2c6e9a17ac9ed20a6e46873751bcdf56d331e13", - "bytes": 28750, - "width": 1280, - "height": 720 - }, - { - "name": "aui-resize-03-mid-drag.png", - "engine": "chromium", - "sha256": "972aaa93464b1772438cc8640f02ad113c4fb90cc500d6cba1684974614bd093", - "bytes": 28841, - "width": 1280, - "height": 720 - }, - { - "name": "aui-resize-04-after.png", - "engine": "chromium", - "sha256": "8650595db7868c5544dfa2434bf845c0188b935fa014527a00a44dace268124b", - "bytes": 28843, - "width": 1280, - "height": 720 - }, - { - "name": "auinotebook-01-loaded.png", - "engine": "chromium", - "sha256": "80c2bc70925f46be1630c490a1a3e3c735d4809aec420909f5a058510313b605", - "bytes": 44737, - "width": 1280, - "height": 720 - }, - { - "name": "auinotebook-02-tab-switch.png", - "engine": "chromium", - "sha256": "0040913c308ec96ba64433a7d16493155852a2c019eee50e8b690c65d547b89e", - "bytes": 45173, - "width": 1280, - "height": 720 - }, - { - "name": "auinotebook-03-add-tab.png", - "engine": "chromium", - "sha256": "5f1cdc8401251589570794310ef88065a88144a0be729479faf93e1c576affbd", - "bytes": 38664, - "width": 1280, - "height": 720 - }, - { - "name": "auinotebook-04-remove-tab.png", - "engine": "chromium", - "sha256": "114b86c5d55c04170dd8ff2de64f082099cc166f49984366e055b3d53dc4a51d", - "bytes": 46391, - "width": 1280, - "height": 720 - }, - { - "name": "auinotebook-05-tab-style.png", - "engine": "chromium", - "sha256": "ad5809808d5ecbea4fc86f1049523ce55eb84f547283d65fc826b4d90b384c50", - "bytes": 46574, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-01-loaded.png", - "engine": "chromium", - "sha256": "fdd47d8f19db4a3ef9ebddeb054be406dd6c24203cf78a02e67e4318c031be0e", - "bytes": 35720, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-02-toolbar-click.png", - "engine": "chromium", - "sha256": "aac4d3e9e7d08f47c47fd9bc3ce08a65a32d023b1e7d4f984a6507345da4ab43", - "bytes": 38468, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-03-toggle.png", - "engine": "chromium", - "sha256": "72e2e587c4bcd525ac3bb4727e1f729187ef156c346e0d24190ac5dfd8cf5620", - "bytes": 37136, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-04-multi-toggle.png", - "engine": "chromium", - "sha256": "6a09c1aa935d996fc3ee111f228274238cfaf84bb2d1cc19b528f445d128c4a8", - "bytes": 38601, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-05-enable-toggle.png", - "engine": "chromium", - "sha256": "0d985c3b70c3444e2374d6ef0152ba93f4b297933b8600153bc20db4bdb69129", - "bytes": 37269, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-06-shapes.png", - "engine": "chromium", - "sha256": "ef497f0b9a1c998d6f9eecfdc1ac9e7c37ed88d66e2f31f17ce5e4c31c582866", - "bytes": 40257, - "width": 1280, - "height": 720 - }, - { - "name": "bitmapbuttons-07-artprovider.png", - "engine": "chromium", - "sha256": "914163b441cdf861b82c41416ea5936d88816736ae00f4f33b0bd49438e92107", - "bytes": 38059, - "width": 1280, - "height": 720 - }, - { - "name": "bitmask.png", - "engine": "chromium", - "sha256": "b377296d2ed20c6f7bf9959a9d5105bfed8b0b84822934c80039ea37178810e8", - "bytes": 52974, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-before-switch.png", - "engine": "chromium", - "sha256": "aa488ec1f9167980667f9d8a05b9bc52f972ae7585a3c724cd1c57177e8012d5", - "bytes": 68846, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-color-code.png", - "engine": "chromium", - "sha256": "195b98722464d71e28edb528a8645c8cd2a3f74932c44b9d50f332f75d6e8803", - "bytes": 71587, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-loaded.png", - "engine": "chromium", - "sha256": "aa488ec1f9167980667f9d8a05b9bc52f972ae7585a3c724cd1c57177e8012d5", - "bytes": 68846, - "width": 1280, - "height": 720 - }, - { - "name": "calendar-01-loaded.png", - "engine": "chromium", - "sha256": "96dcfca53f8a31717ecdf2701d6d6f4442b56d8d15a536a47e158f5d11ba736b", - "bytes": 41850, - "width": 1280, - "height": 720 - }, - { - "name": "calendar-02-select-date.png", - "engine": "chromium", - "sha256": "daccdc5e2994d4f5774efc96a232d30328c916ccd73e6fe08121f3165fd4d5a9", - "bytes": 45185, - "width": 1280, - "height": 720 - }, - { - "name": "calendar-03-next-month.png", - "engine": "chromium", - "sha256": "c9ac7a97caa2fca1b51f61198fffb025a932e557a4fab49674d3b309418fe383", - "bytes": 44001, - "width": 1280, - "height": 720 - }, - { - "name": "calendar-04-prev-month.png", - "engine": "chromium", - "sha256": "ec11f6fad364e011abad25e8d922864929624586b919227a88e0c796bb6344a8", - "bytes": 43534, - "width": 1280, - "height": 720 - }, - { - "name": "calendar-05-today.png", - "engine": "chromium", - "sha256": "8c5b486bb401a293925aeb46d7abe3c829fc5bd008673154df245501ff0a289f", - "bytes": 45149, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-01-loaded.png", - "engine": "chromium", - "sha256": "3aa56046961871848dc51c8516ea67a6d3e2159449bbcdc9de55f42dcb70ddec", - "bytes": 31734, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-02-copy-clicked.png", - "engine": "chromium", - "sha256": "fd2491c104397c5055ec216bd3023c6ad27389fcc8ce8ba870a6f838e10d86bb", - "bytes": 34601, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-03-paste-clicked.png", - "engine": "chromium", - "sha256": "bd31e04494bf4bbc9b788fb88f74e2694e42138740b07e5b0a3a5563d3ac8987", - "bytes": 40429, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-04-check-clicked.png", - "engine": "chromium", - "sha256": "d4904392e79d3b840a403a00ea01dea433be8f2542f03d588300c0e006d5af55", - "bytes": 39186, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-05-clear-clicked.png", - "engine": "chromium", - "sha256": "304576cbdff87140d27e2c796e853cf7646603d45fc975e2a68ff0b7210e1f4f", - "bytes": 39691, - "width": 1280, - "height": 720 - }, - { - "name": "clipboard-06-full-flow.png", - "engine": "chromium", - "sha256": "2ad6763715076996901156240a946b864e14ca3356ed22bb27bea7974b152507", - "bytes": 49183, - "width": 1280, - "height": 720 - }, - { - "name": "collapse-relayout-01-loaded.png", - "engine": "chromium", - "sha256": "ca5091fa4579e6272076ae18085b85c580cabfc0d0b51b2b3ca48a6f3f367f9d", - "bytes": 23744, - "width": 1280, - "height": 720 - }, - { - "name": "collapse-relayout-02-expanded.png", - "engine": "chromium", - "sha256": "e5a5330335a2be50a893aa0192d9d9f2390d99c1c74d1a51657fb1699f5f8a92", - "bytes": 28580, - "width": 1280, - "height": 720 - }, - { - "name": "collapsible-01-loaded.png", - "engine": "chromium", - "sha256": "782ab189948b2da5471c27776476f8bd3a6421908ee0831a301df81db5ff3442", - "bytes": 38001, - "width": 1280, - "height": 720 - }, - { - "name": "collapsible-02-panes.png", - "engine": "chromium", - "sha256": "782ab189948b2da5471c27776476f8bd3a6421908ee0831a301df81db5ff3442", - "bytes": 38001, - "width": 1280, - "height": 720 - }, - { - "name": "collapsible-03-expanded.png", - "engine": "chromium", - "sha256": "782ab189948b2da5471c27776476f8bd3a6421908ee0831a301df81db5ff3442", - "bytes": 38001, - "width": 1280, - "height": 720 - }, - { - "name": "collapsible-04-expand-all.png", - "engine": "chromium", - "sha256": "da18f9825f03886035a67746a73e777859bf05f02e868c44c07866f2b8419f02", - "bytes": 49651, - "width": 1280, - "height": 720 - }, - { - "name": "collapsible-05-collapse-all.png", - "engine": "chromium", - "sha256": "60441feca9bdd64daf2f4b459579d171dd36301222a2a138b00dd9abfdd1e039", - "bytes": 32486, - "width": 1280, - "height": 720 - }, - { - "name": "contextmenu-01-open.png", - "engine": "chromium", - "sha256": "117fab7062f34b707c512caa0d6a8cbf70b28f5c29466fbe688be748c1761ce5", - "bytes": 15382, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-01-loaded.png", - "engine": "chromium", - "sha256": "8ca29fadc686ce62fdc035f0968b185103cd983f516eadb4e5eb375e37ed7586", - "bytes": 125122, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-02-summary.png", - "engine": "chromium", - "sha256": "8ca29fadc686ce62fdc035f0968b185103cd983f516eadb4e5eb375e37ed7586", - "bytes": 125122, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-nested-01-loaded.png", - "engine": "chromium", - "sha256": "7421a1568c9ff5c51df6d138b4de8e582ad8616bc8ae8fef9ea6ed61955718c2", - "bytes": 92012, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-nested-02-summary.png", - "engine": "chromium", - "sha256": "7421a1568c9ff5c51df6d138b4de8e582ad8616bc8ae8fef9ea6ed61955718c2", - "bytes": 92012, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-01-loaded.png", - "engine": "chromium", - "sha256": "b6bbe192c7942fac1729ded46107042989726fefbb2ec4138c4d989ff94d2068", - "bytes": 85061, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-02-list-populated.png", - "engine": "chromium", - "sha256": "b6bbe192c7942fac1729ded46107042989726fefbb2ec4138c4d989ff94d2068", - "bytes": 85061, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-03-list-selected.png", - "engine": "chromium", - "sha256": "72f767ae63d9c9cd2b829d283504cbdebc237deaa561ab0823d31da3178d23c8", - "bytes": 87769, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-04-list-add.png", - "engine": "chromium", - "sha256": "e548e05eeca5652dccaa7e0055363c8b9cd607897e0f7e7dcb98d0fd7d97ec1b", - "bytes": 86848, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-05-tree-tab.png", - "engine": "chromium", - "sha256": "24ffb5c04e9ac045b4dea7919f00ec7f2b58866673c7dcd7e87562964551c83f", - "bytes": 38002, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-06-tree-selected.png", - "engine": "chromium", - "sha256": "3fa3937040259dbd1ff22d4d3598999b4b76d803b4f5a952c13b8e2bebd1140e", - "bytes": 39966, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-07-tree-expanded.png", - "engine": "chromium", - "sha256": "1e14be6bf81cfab8738f3ce0a96838d1d1f5797d1bcfc0073ba3028c729e7e4d", - "bytes": 49344, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-08-tree-collapsed.png", - "engine": "chromium", - "sha256": "49dfbe819d6726e9de1c3fae34c2e27604f76ff692f7a47129954c0cf094a171", - "bytes": 37703, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-09-column-click.png", - "engine": "chromium", - "sha256": "2ada8bb4371279410ff0c6bd57a89f011f4ec13dbff2dce4f0ce6e7f6958b501", - "bytes": 87944, - "width": 1280, - "height": 720 - }, - { - "name": "dataview-10-scrolled.png", - "engine": "chromium", - "sha256": "4ab56605ea4f5262f2e1bd64d994572c990b284664e285ae6d40f7b3627d8bda", - "bytes": 83713, - "width": 1280, - "height": 720 - }, - { - "name": "dataviewvirtual-01-loaded.png", - "engine": "chromium", - "sha256": "3d34dd84b50c46676ee15890f209c78b818e6730e9bd6c6aa7a407a8827979cc", - "bytes": 127648, - "width": 1280, - "height": 720 - }, - { - "name": "dataviewvirtual-02-large-dataset.png", - "engine": "chromium", - "sha256": "3d34dd84b50c46676ee15890f209c78b818e6730e9bd6c6aa7a407a8827979cc", - "bytes": 127648, - "width": 1280, - "height": 720 - }, - { - "name": "dataviewvirtual-03-scroll.png", - "engine": "chromium", - "sha256": "4f0d859f58c27fbbb17f565a4f87c884c2b23b342e7616feac39e9b92d922fcb", - "bytes": 138109, - "width": 1280, - "height": 720 - }, - { - "name": "dataviewvirtual-04-selection.png", - "engine": "chromium", - "sha256": "3d34dd84b50c46676ee15890f209c78b818e6730e9bd6c6aa7a407a8827979cc", - "bytes": 127648, - "width": 1280, - "height": 720 - }, - { - "name": "dataviewvirtual-05-zone-manager.png", - "engine": "chromium", - "sha256": "3d34dd84b50c46676ee15890f209c78b818e6730e9bd6c6aa7a407a8827979cc", - "bytes": 127648, - "width": 1280, - "height": 720 - }, - { - "name": "dialog-01-loaded.png", - "engine": "chromium", - "sha256": "c3a47f81680860f795523c85a00b44406df2c3f84e8cb5124465121795165d18", - "bytes": 29930, - "width": 1280, - "height": 720 - }, - { - "name": "dialog-02-info-clicked.png", - "engine": "chromium", - "sha256": "68945796e6c0d6d6fcd78230fa27216ea7822c9dd5956a489d7fba0603387941", - "bytes": 43319, - "width": 1280, - "height": 1146 - }, - { - "name": "dialog-03-yesno-clicked.png", - "engine": "chromium", - "sha256": "359954015822573411ed52b4e2abec5c8cbd2815cc64c231e9c0019509a9a97c", - "bytes": 45221, - "width": 1280, - "height": 1146 - }, - { - "name": "dialog-04-error-clicked.png", - "engine": "chromium", - "sha256": "0819956debc59d567dcdc2930b32de7a524754db667f7ed81cd3b3ed52d8b25e", - "bytes": 42198, - "width": 1280, - "height": 1146 - }, - { - "name": "dialog-05-custom-clicked.png", - "engine": "chromium", - "sha256": "a285bbe8bd3c1dbd20e3e6b013508320c954835c67b04d62362321323724f615", - "bytes": 43477, - "width": 1280, - "height": 922 - }, - { - "name": "dialogs-00-initial.png", - "engine": "chromium", - "sha256": "3419d71893837b3c09157778b442bd8d4404757b26b44f904e1cfdd6284fcfb3", - "bytes": 28996, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-01-tab-selected.png", - "engine": "chromium", - "sha256": "d7cae66608c0e8d8e4445c6f46e17cedf45b58b3e0281d1a2c6a54e768d0510d", - "bytes": 42447, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-custom-closed.png", - "engine": "chromium", - "sha256": "2bdb002868246fe38e480ca4fa5de8c887b10083cba919c8c4106803f444d56d", - "bytes": 58146, - "width": 1280, - "height": 922 - }, - { - "name": "dialogs-custom-open.png", - "engine": "chromium", - "sha256": "12a4b20144f153d1afae6aaf32663cc0780f2989654725e3090df8534b57871f", - "bytes": 58166, - "width": 1280, - "height": 922 - }, - { - "name": "dialogs-full-flow.png", - "engine": "chromium", - "sha256": "b58a0ca7954518ba0f7efb20d80b1f668bdde32b0660b5298db98a20bc666cb8", - "bytes": 67850, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-error-closed.png", - "engine": "chromium", - "sha256": "25e8da8db33e67e7023b10b33dfc3b0fde8c1a416f69b752834e2879e2840e12", - "bytes": 57203, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-error-open.png", - "engine": "chromium", - "sha256": "823653e5978443f298c60ade95a53e2efcd34822fcf0057f4ced86ddcb40724f", - "bytes": 57403, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-info-closed.png", - "engine": "chromium", - "sha256": "036bcd75c028683c240fec3494100dc802914e38465dc2a2b615615488a65d2e", - "bytes": 59345, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-info-open.png", - "engine": "chromium", - "sha256": "e8c67b79d7dbf32df7f1a8cbe725a3d77854549e8d448a65b5d09b09865b4bb5", - "bytes": 59744, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-yesno-closed.png", - "engine": "chromium", - "sha256": "095f80354dd993d73e50a8ccd1c0bb33a75d508c666a459011feaa1ed26b4fa4", - "bytes": 65092, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-msgbox-yesno-open.png", - "engine": "chromium", - "sha256": "bb0a05732941b1cbab81d91c1e3b3edb8b139c5c253c007ba477d24d53ac1d3c", - "bytes": 59734, - "width": 1280, - "height": 1146 - }, - { - "name": "dialogs-timer-initial.png", - "engine": "chromium", - "sha256": "d6e308b7403234dad04e106773a1712862d03858d0348a83c054acf360aee910", - "bytes": 42467, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-timer-multiple.png", - "engine": "chromium", - "sha256": "74ee6563bdabb56539cbd09e765e3f478ec90fc559f522261b84b665202d0cef", - "bytes": 49495, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-timer-running.png", - "engine": "chromium", - "sha256": "12b1d68aefaf452f806ea155f4f26ff46879a74d048dee748e04c9449ca20145", - "bytes": 48314, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-timer-started.png", - "engine": "chromium", - "sha256": "3a5190a2eb2fd6c4fd327c291442cb5e5b874440613fb658cb586d0b45c205c5", - "bytes": 45885, - "width": 1280, - "height": 720 - }, - { - "name": "dialogs-timer-stopped.png", - "engine": "chromium", - "sha256": "1846666a73dd41997fd9e39fea78cfe4348a6ddc695b14eb8793905cc78cb72e", - "bytes": 47822, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-01-loaded.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-02-handlers.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-03-dragenter.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-04-dragleave.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-05-drop.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-06-file-written.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-07-event-fired.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-08-multiple-files.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "dnd-09-with-file.png", - "engine": "chromium", - "sha256": "037677110c9a53f4d4fac4dcad540e5a54281bae9ccd1f32db89863cce4b9e1e", - "bytes": 35997, - "width": 1280, - "height": 720 - }, - { - "name": "earlysize-01-loaded.png", - "engine": "chromium", - "sha256": "14a5404eea7c6d14bcc59d1b54e0e3f981a71391cbcf6571906da0a4a71eda8a", - "bytes": 27335, - "width": 1280, - "height": 720 - }, - { - "name": "earlysize-02-result.png", - "engine": "chromium", - "sha256": "14a5404eea7c6d14bcc59d1b54e0e3f981a71391cbcf6571906da0a4a71eda8a", - "bytes": 27335, - "width": 1280, - "height": 720 - }, - { - "name": "eefpsel-01-boot.png", - "engine": "chromium", - "sha256": "c931c622061459244e2acb0e37f53f3048bb992d289cc5024ba5ecb372c1783c", - "bytes": 62646, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-00-small.png", - "engine": "chromium", - "sha256": "ef9a510f84b97461009ed758cce2e38ac5ce693f01978a21f6c813f4aeb52969", - "bytes": 57665, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-01-full.png", - "engine": "chromium", - "sha256": "16867e67d2865c493b875b8f66016b34f4c37387c13f2e1c54a7ca786f0c651e", - "bytes": 57918, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-02-45.png", - "engine": "chromium", - "sha256": "b1b8124360bf41aa19a40d37a46d2db4d84a7473b9a0f6b4ca6b38689ccddda7", - "bytes": 61814, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-03-small-again.png", - "engine": "chromium", - "sha256": "ef9a510f84b97461009ed758cce2e38ac5ce693f01978a21f6c813f4aeb52969", - "bytes": 57665, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-00-before-tool-click.png", - "engine": "chromium", - "sha256": "c075b77806ba67cb3150244496c204232ce257bf786022f4ab191e7456cf8f15", - "bytes": 55977, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-01-after-click.png", - "engine": "chromium", - "sha256": "c8ef0c1ebb54a7cba05bde0bbbf402fc8fc58d5a9bda49514ad720035c65e2e7", - "bytes": 57675, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-02-after-drawing.png", - "engine": "chromium", - "sha256": "0c068e0a5b94c203f4aed67e446362ccb33de386c78d461073b67a2e614eb6ff", - "bytes": 58245, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-load-rendered.png", - "engine": "chromium", - "sha256": "be06d89a4301af681734d06bea4236d8b2b494f5fbf17da5f5b48d8fa3b6f976", - "bytes": 61762, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-loaded-css.png", - "engine": "chromium", - "sha256": "c075b77806ba67cb3150244496c204232ce257bf786022f4ab191e7456cf8f15", - "bytes": 55977, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-loaded.png", - "engine": "chromium", - "sha256": "c075b77806ba67cb3150244496c204232ce257bf786022f4ab191e7456cf8f15", - "bytes": 55977, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-sim-frame.png", - "engine": "chromium", - "sha256": "f8716d440922de98d596fc02fe636c6c74cac5e39754450a44394dcb205f532f", - "bytes": 87718, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-sim-plot.png", - "engine": "chromium", - "sha256": "fbe17f157cf27b5057df83dc7570deff00b4f25ce732cb8d495ae586556cbd3f", - "bytes": 95282, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-url-regex.png", - "engine": "chromium", - "sha256": "6f5432b5656741d9386d13aafd949bf15735e038b6ad9812dfaaccd15d0f5632", - "bytes": 61974, - "width": 1280, - "height": 720 - }, - { - "name": "filedialog-01-loaded.png", - "engine": "chromium", - "sha256": "404f80e779eb2909dad48a942caa7b4d8a9a41d8f3d0338029808b8887fe85dc", - "bytes": 22658, - "width": 1280, - "height": 720 - }, - { - "name": "filedialog-02-open-clicked.png", - "engine": "chromium", - "sha256": "d6aeebf7d70df88b4ac04c4688746d500533d89feb87c524853bb8c5f5c74e61", - "bytes": 37476, - "width": 1280, - "height": 1225 - }, - { - "name": "filedialog-03-save-clicked.png", - "engine": "chromium", - "sha256": "9a3a781de20175409e8d5605405ccdc77eede2a106f333b90b6c0d1e81fe6c74", - "bytes": 38122, - "width": 1280, - "height": 1214 - }, - { - "name": "filedialog-04-multiple-clicked.png", - "engine": "chromium", - "sha256": "884a0277ec767da898e74e9548da1a42dc51e726da95c674a1e8e6c0a653e4ed", - "bytes": 39225, - "width": 1280, - "height": 1225 - }, - { - "name": "filedialog-05-all-buttons.png", - "engine": "chromium", - "sha256": "4f3068d49795c8f62cc30a4b975829b6be6df37ec375255f43c706280d76f3dd", - "bytes": 45549, - "width": 1280, - "height": 1225 - }, - { - "name": "filedlg-folder-nav.png", - "engine": "chromium", - "sha256": "d9703625bd9b4b0b9def909aaaef478ae567082a1384a138800fc9f3a3f75bf3", - "bytes": 37628, - "width": 1280, - "height": 1225 - }, - { - "name": "fontenum.png", - "engine": "chromium", - "sha256": "c5dcc2b114f0bd003d6cd5e9a6d9eaef8413cc2e2f0fe2a02345a0a731af0c02", - "bytes": 134301, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-eeschema.html.png", - "engine": "chromium", - "sha256": "c075b77806ba67cb3150244496c204232ce257bf786022f4ab191e7456cf8f15", - "bytes": 55977, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-footprint_editor.html.png", - "engine": "chromium", - "sha256": "b5400a538dac19a92d8b32f9400ff47a23cd756cab402fc3bdb2d01057962b7c", - "bytes": 94521, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-pcbnew.html.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-symbol_editor.html.png", - "engine": "chromium", - "sha256": "91bd83a6ce37352cbfb6e63e84cdb05e308ee2b51ed81654210127a7400778df", - "bytes": 42706, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-01-loaded.png", - "engine": "chromium", - "sha256": "047f32ad58c6b2e6f1d2c31b74c0e9594e984cf0e3b8be223d550aa35d5768df", - "bytes": 38327, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-02-metrics.png", - "engine": "chromium", - "sha256": "047f32ad58c6b2e6f1d2c31b74c0e9594e984cf0e3b8be223d550aa35d5768df", - "bytes": 38327, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-00-loaded.png", - "engine": "chromium", - "sha256": "4695bb5d42ddb45a95c674654ced552a078e2a5f18151f9bbfa3193c50972f37", - "bytes": 85943, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-01-dialog.png", - "engine": "chromium", - "sha256": "771a22876cc5edc0e1478b47cbf76d0775235e835cca18082fc56fa78489508b", - "bytes": 105906, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-02-reopened.png", - "engine": "chromium", - "sha256": "0802e16507d749f079214ebd3fd12b4eb19e27b1e5853f8b2f2e4a65d303a799", - "bytes": 105905, - "width": 1280, - "height": 720 - }, - { - "name": "grid-tab-final.png", - "engine": "chromium", - "sha256": "dbb0f95a668c5ccd98c4b60cc89e0e3e6c0c5fbe1c436083a469fcdc98fa4bc8", - "bytes": 38657, - "width": 1280, - "height": 720 - }, - { - "name": "gridedit-01-loaded.png", - "engine": "chromium", - "sha256": "0389f476e026a8b617706d73abf87bf0ce2692284823668f45abd68cc7370d54", - "bytes": 58745, - "width": 1280, - "height": 720 - }, - { - "name": "gridedit-02-select-cell.png", - "engine": "chromium", - "sha256": "71925b1c26df3f87075b241ecb85d37a6cafaf46a2d825ae13ded35183754908", - "bytes": 60189, - "width": 1280, - "height": 720 - }, - { - "name": "gridedit-03-edit-cell.png", - "engine": "chromium", - "sha256": "eef0d57565f49a0a019762cc51a833923446059cc969dc3d4227b880ce143a77", - "bytes": 61936, - "width": 1280, - "height": 720 - }, - { - "name": "gridedit-04-add-row.png", - "engine": "chromium", - "sha256": "63b6e7d9c39ae26dce04e52c2e9e1d7e852e36a117f74addb952da5a68c84904", - "bytes": 61290, - "width": 1280, - "height": 720 - }, - { - "name": "gridedit-05-delete-row.png", - "engine": "chromium", - "sha256": "453abe7e570211a532b677cdca1b8d87fe0830d0c499cba783106978a4721f41", - "bytes": 59252, - "width": 1280, - "height": 720 - }, - { - "name": "gridrenderers-01-loaded.png", - "engine": "chromium", - "sha256": "4cafabc637f0f853a87b6123a8bbb80be888361224d2f928b6b47a4887de106e", - "bytes": 39369, - "width": 1280, - "height": 720 - }, - { - "name": "gridrenderers-02-color-cells.png", - "engine": "chromium", - "sha256": "4cafabc637f0f853a87b6123a8bbb80be888361224d2f928b6b47a4887de106e", - "bytes": 39369, - "width": 1280, - "height": 720 - }, - { - "name": "gridrenderers-03-icon-text.png", - "engine": "chromium", - "sha256": "b4583f75ca74c74d25494f17aa6dc944aeafa775cab82855107ca4dbfb7fe79d", - "bytes": 70015, - "width": 1280, - "height": 720 - }, - { - "name": "gridrenderers-04-striped.png", - "engine": "chromium", - "sha256": "8e1263f236dcf036a287749ea002a132d8614d04d1e1fd51d161c6a9d16f98a6", - "bytes": 45026, - "width": 1280, - "height": 720 - }, - { - "name": "gridrenderers-05-checkbox-toggle.png", - "engine": "chromium", - "sha256": "944e00a7ad464543de878c42bd3a441df872644d78c90d0e4c6aabfcb38a6904", - "bytes": 45087, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-01-loaded.png", - "engine": "chromium", - "sha256": "16a24e90cef87b8cb2c4804170667c3afcac8164e3bd17372a41fc9c88d47896", - "bytes": 49710, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-02-basic-content.png", - "engine": "chromium", - "sha256": "16a24e90cef87b8cb2c4804170667c3afcac8164e3bd17372a41fc9c88d47896", - "bytes": 49710, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-03-tables.png", - "engine": "chromium", - "sha256": "14985c6e77bdddb538624c182d88a7a93bba2b60f9991c35abce3b3ceb1ed824", - "bytes": 55093, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-04-long-content.png", - "engine": "chromium", - "sha256": "ee6a3c4e6776845fa44c3b64772976605e6b599813b4b6d29f76070251204799", - "bytes": 73824, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-05-kicad-about.png", - "engine": "chromium", - "sha256": "05419a02cdb3465f44ef647f74ea5a02282d50ac9de52d570bd1d65e685087f1", - "bytes": 53843, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-06-link-clicked.png", - "engine": "chromium", - "sha256": "cf12518102023a904fe24ec1b922145ff0b7d72755f55052ed8bd61be8ae7a48", - "bytes": 52005, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-07-scrolled.png", - "engine": "chromium", - "sha256": "e5eb4fc45d421fcee4eea0a9f96f98e3d8ba9deb4365ce3abc61ee5541f1fc58", - "bytes": 80340, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-08a-basic.png", - "engine": "chromium", - "sha256": "d7faf13f5a0e3d155cde57eef47770a8b0de2ede91e91f2f63578a408c7ea50f", - "bytes": 52119, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-08b-tables.png", - "engine": "chromium", - "sha256": "498dd1dcc41e83df06badb9b778d5c3bb9e6427a2cb0a6f30a7b299bbf7b8787", - "bytes": 56926, - "width": 1280, - "height": 720 - }, - { - "name": "htmlwin-08c-about.png", - "engine": "chromium", - "sha256": "9b457fb87490c2b1268b0cd83139c6a9a718d7e55f49bf756d191295e2b657f7", - "bytes": 57456, - "width": 1280, - "height": 720 - }, - { - "name": "import-settings-modal-stack.png", - "engine": "chromium", - "sha256": "9d8c0242c934e25ddbc8b07836a972050960efb0626d655c40b02d6f292f1e0a", - "bytes": 148096, - "width": 1280, - "height": 720 - }, - { - "name": "infobar-01-loaded.png", - "engine": "chromium", - "sha256": "442b045e41d3a77de38281f3ea58e89514344384bc0191c87e7bbb4acc01f095", - "bytes": 63306, - "width": 1280, - "height": 720 - }, - { - "name": "infobar-02-info.png", - "engine": "chromium", - "sha256": "66fba002067ca9fe3d8516ddac9826a292cdba05776d7be13a48dd945d8463dc", - "bytes": 70159, - "width": 1280, - "height": 720 - }, - { - "name": "infobar-03-warning.png", - "engine": "chromium", - "sha256": "862874d6c3794fc996dcca54ee55692b42295d5c58c15f423bf8cad820cd0764", - "bytes": 69899, - "width": 1280, - "height": 720 - }, - { - "name": "infobar-04-error.png", - "engine": "chromium", - "sha256": "8be3eda782608abb957f55398fdbf432bf3231d92bbf96333b2f736768e9a850", - "bytes": 69037, - "width": 1280, - "height": 720 - }, - { - "name": "infobar-05-dismiss.png", - "engine": "chromium", - "sha256": "8985031a75c660b899a4661fbdb68cfd8a4ee9076dfeb8ba0f985e4f47f82946", - "bytes": 66274, - "width": 1280, - "height": 720 - }, - { - "name": "layout-01-loaded.png", - "engine": "chromium", - "sha256": "b6d2e57e0bceba6d43204bd8a9ae2f34d3def0628d30b59cd06c48201cdacbc9", - "bytes": 51899, - "width": 1280, - "height": 720 - }, - { - "name": "layout-02-splitter.png", - "engine": "chromium", - "sha256": "b6d2e57e0bceba6d43204bd8a9ae2f34d3def0628d30b59cd06c48201cdacbc9", - "bytes": 51899, - "width": 1280, - "height": 720 - }, - { - "name": "layout-03-sash-dragged.png", - "engine": "chromium", - "sha256": "b1f71756a0a75a43967ab12dc3f9ce4b502916a84c2bb3a797a0411268277613", - "bytes": 54648, - "width": 1280, - "height": 720 - }, - { - "name": "layout-04-scrolled-left.png", - "engine": "chromium", - "sha256": "b6d2e57e0bceba6d43204bd8a9ae2f34d3def0628d30b59cd06c48201cdacbc9", - "bytes": 51899, - "width": 1280, - "height": 720 - }, - { - "name": "layout-05-scrolled-right.png", - "engine": "chromium", - "sha256": "b6d2e57e0bceba6d43204bd8a9ae2f34d3def0628d30b59cd06c48201cdacbc9", - "bytes": 51899, - "width": 1280, - "height": 720 - }, - { - "name": "layout-06-combined.png", - "engine": "chromium", - "sha256": "b1f71756a0a75a43967ab12dc3f9ce4b502916a84c2bb3a797a0411268277613", - "bytes": 54648, - "width": 1280, - "height": 720 - }, - { - "name": "listctrl-01-loaded.png", - "engine": "chromium", - "sha256": "28f90168cbeb3f2b1edce35904cdfb208b60244a099d51834b41b48558e12b35", - "bytes": 80345, - "width": 1280, - "height": 720 - }, - { - "name": "listctrl-02-virtual.png", - "engine": "chromium", - "sha256": "28f90168cbeb3f2b1edce35904cdfb208b60244a099d51834b41b48558e12b35", - "bytes": 80345, - "width": 1280, - "height": 720 - }, - { - "name": "listctrl-03-columns.png", - "engine": "chromium", - "sha256": "28f90168cbeb3f2b1edce35904cdfb208b60244a099d51834b41b48558e12b35", - "bytes": 80345, - "width": 1280, - "height": 720 - }, - { - "name": "listctrl-04-selected.png", - "engine": "chromium", - "sha256": "aba59fe63ccd3a908e9a49dbdf43b00424ad707378f4368135cb54d74a02da75", - "bytes": 80788, - "width": 1280, - "height": 720 - }, - { - "name": "listctrl-05-scrolled.png", - "engine": "chromium", - "sha256": "dc540b58d96d72ddb97728c4121de566b3e35c433efce74c003b244d8dd6405e", - "bytes": 85449, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave-00-pcbnew-ready.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave-01-dialog-open.png", - "engine": "chromium", - "sha256": "84df0436dd6a008fe2d9f700d264d2e268ec3185803b5aefff22ee491af3a9f0", - "bytes": 121801, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave.png", - "engine": "chromium", - "sha256": "c77a82e6fc000bb7d1f875470bae9614ee58ed71d808eb261c6525968b19cc99", - "bytes": 110274, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer-00-pcbnew-ready.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer-01-dialog-open.png", - "engine": "chromium", - "sha256": "d20cdd3276a509161f0e5a229be73eff8d16ee9a3e4243e6f8a1a469ce87fc5b", - "bytes": 122058, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer.png", - "engine": "chromium", - "sha256": "2bb785970e78ac20dafe353a4c628161d6406f517045f9e7674bed75a466dca8", - "bytes": 252106, - "width": 1280, - "height": 720 - }, - { - "name": "logerror-01-initial.png", - "engine": "chromium", - "sha256": "c208c264da509e122308af765b3955a458846db8c44f93409d7666c04bb12706", - "bytes": 49677, - "width": 1280, - "height": 720 - }, - { - "name": "logerror-02-before-error.png", - "engine": "chromium", - "sha256": "c208c264da509e122308af765b3955a458846db8c44f93409d7666c04bb12706", - "bytes": 49677, - "width": 1280, - "height": 720 - }, - { - "name": "logerror-03-single-error-dialog.png", - "engine": "chromium", - "sha256": "df82e13e84a3275c9ff981883acdda7400d9b14703e95ff4a2b7d0303b18bbb5", - "bytes": 69537, - "width": 1280, - "height": 1136 - }, - { - "name": "logerror-04-multiple-errors-dialog.png", - "engine": "chromium", - "sha256": "89b287c776478cdc697ab5d8a20e344c6d12ebfde1e1d18e9407b35dda0fdfd6", - "bytes": 70165, - "width": 1280, - "height": 1129 - }, - { - "name": "logerror-05-mixed-levels.png", - "engine": "chromium", - "sha256": "22980a15cf3b1dfff4833ec9603d2b0a95fb3a686aaab8ef00e937f70272ce0c", - "bytes": 65777, - "width": 1280, - "height": 1129 - }, - { - "name": "maximize-01-loaded.png", - "engine": "chromium", - "sha256": "192b41571b6c825396049972e8aedc66c7bbe14f2a4814804d6df5e95c98f20e", - "bytes": 30329, - "width": 1280, - "height": 720 - }, - { - "name": "maximize-02-fullscreen.png", - "engine": "chromium", - "sha256": "192b41571b6c825396049972e8aedc66c7bbe14f2a4814804d6df5e95c98f20e", - "bytes": 30329, - "width": 1280, - "height": 720 - }, - { - "name": "maximize-03-canvas.png", - "engine": "chromium", - "sha256": "192b41571b6c825396049972e8aedc66c7bbe14f2a4814804d6df5e95c98f20e", - "bytes": 30329, - "width": 1280, - "height": 720 - }, - { - "name": "menu-01-loaded.png", - "engine": "chromium", - "sha256": "7dd194e7dd380f85f741ef3b6c501dddf661cc1f71e1cbf62aeb6dba8b41f569", - "bytes": 24820, - "width": 1280, - "height": 720 - }, - { - "name": "menu-02-menubar.png", - "engine": "chromium", - "sha256": "7dd194e7dd380f85f741ef3b6c501dddf661cc1f71e1cbf62aeb6dba8b41f569", - "bytes": 24820, - "width": 1280, - "height": 720 - }, - { - "name": "menu-03-file-clicked.png", - "engine": "chromium", - "sha256": "da3fce7616932be82861a98405922932ef8883c8c134de02ba2f5209514bf64e", - "bytes": 23929, - "width": 1280, - "height": 720 - }, - { - "name": "menu-04-edit-clicked.png", - "engine": "chromium", - "sha256": "f72a90ef8ddf7ce94ffb5b5e6efd7730ab4e05667d0a029b1d444d5738a551f7", - "bytes": 25762, - "width": 1280, - "height": 720 - }, - { - "name": "menu-05-all-menus.png", - "engine": "chromium", - "sha256": "ca1b688681e3c5ddfb776a851c5d7c9272857c452feace02cd4b77b902758a45", - "bytes": 27013, - "width": 1280, - "height": 720 - }, - { - "name": "mobile-chrome.png", - "engine": "chromium", - "sha256": "c6975eb91549be5ccd78915472c5f6fdba4471f5f294bae118101fe1a74352de", - "bytes": 26971, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pan-00-base.png", - "engine": "chromium", - "sha256": "79acfbe68ced93f72e35959dfbc249d9b050282080941cf0e02d063aa1b5e98c", - "bytes": 26887, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pan-01-panned.png", - "engine": "chromium", - "sha256": "10fe9d61b4cbacda47c8f8f708ce202ac1b7b72293aee86ae2cec6b668969dc2", - "bytes": 24139, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pan-02-restored.png", - "engine": "chromium", - "sha256": "c6975eb91549be5ccd78915472c5f6fdba4471f5f294bae118101fe1a74352de", - "bytes": 26971, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pinch-00-base.png", - "engine": "chromium", - "sha256": "f9d0571f1d3b2c63bfac2e3f8fe90f4bd6afee058712035335f07f301f7c9c0d", - "bytes": 26811, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pinch-01-zoomed.png", - "engine": "chromium", - "sha256": "9c5d05f907971d9972df5e089d9df9e389148d1c7ba992073f454be412621ff9", - "bytes": 60693, - "width": 412, - "height": 839 - }, - { - "name": "mobile-pinch-02-restored.png", - "engine": "chromium", - "sha256": "79acfbe68ced93f72e35959dfbc249d9b050282080941cf0e02d063aa1b5e98c", - "bytes": 26887, - "width": 412, - "height": 839 - }, - { - "name": "mobile-toggle-rehidden.png", - "engine": "chromium", - "sha256": "d5c223408d0dd42cd0c2143f8536af619745e1900eab2bf4cfda2c0338d0df57", - "bytes": 32145, - "width": 412, - "height": 839 - }, - { - "name": "mobile-toggle-shown.png", - "engine": "chromium", - "sha256": "6394e8cf155fd5c08f538efa4eac9f6fb9e67e563a89d934bfedab5bd72f4f94", - "bytes": 121590, - "width": 412, - "height": 839 - }, - { - "name": "modal-01-border.png", - "engine": "chromium", - "sha256": "a285bbe8bd3c1dbd20e3e6b013508320c954835c67b04d62362321323724f615", - "bytes": 43477, - "width": 1280, - "height": 922 - }, - { - "name": "modal-02-before-drag.png", - "engine": "chromium", - "sha256": "7415f2233066c12aefe2cfaa560a281c10c0d3a6f807806095d72ef6402a9e15", - "bytes": 29920, - "width": 1280, - "height": 720 - }, - { - "name": "modal-03-after-drag.png", - "engine": "chromium", - "sha256": "c63dcfc0d7bdfd8b50ac19b4337337b1df22b133401da42b10dc68c68d30a2a3", - "bytes": 39804, - "width": 1280, - "height": 720 - }, - { - "name": "modal-04-before-resize.png", - "engine": "chromium", - "sha256": "7415f2233066c12aefe2cfaa560a281c10c0d3a6f807806095d72ef6402a9e15", - "bytes": 29920, - "width": 1280, - "height": 720 - }, - { - "name": "modal-05-after-resize.png", - "engine": "chromium", - "sha256": "81f3865dd97c6bad00f06d36d953ed82b0a1eafda9f2ff39bd9007e969a409c2", - "bytes": 25236, - "width": 1280, - "height": 720 - }, - { - "name": "notebook-01-scrolled.png", - "engine": "chromium", - "sha256": "6125701515893123955af70ca3908c1f27ca841009bbb1594a364f2a03f2ec33", - "bytes": 29678, - "width": 1280, - "height": 720 - }, - { - "name": "notebook-02-scrolled-again.png", - "engine": "chromium", - "sha256": "6125701515893123955af70ca3908c1f27ca841009bbb1594a364f2a03f2ec33", - "bytes": 29678, - "width": 1280, - "height": 720 - }, - { - "name": "occ-export-dialog.png", - "engine": "chromium", - "sha256": "484b5ad6969f162c0dc344972b957976b8fcf0974f0dbfd09bd049550414acf6", - "bytes": 195991, - "width": 1280, - "height": 720 - }, - { - "name": "occ-export-done.png", - "engine": "chromium", - "sha256": "e2a72d435d0cc4adfd6a787588961db244d68cd6456d1d3a3ec2d460f2da9599", - "bytes": 188412, - "width": 1280, - "height": 720 - }, - { - "name": "occ-probe-done.png", - "engine": "chromium", - "sha256": "fcf0584e219fce4f830acc7c93fd5c76bf63893b0dbd3c20ad1926c79b75e6c7", - "bytes": 6586, - "width": 1280, - "height": 720 - }, - { - "name": "ownerdrawn-01-loaded.png", - "engine": "chromium", - "sha256": "659fbe9e53c69209ce50a5463716848cdbfd1b49600574e7be312ec55731a9e3", - "bytes": 40544, - "width": 1280, - "height": 720 - }, - { - "name": "ownerdrawn-02-layer.png", - "engine": "chromium", - "sha256": "659fbe9e53c69209ce50a5463716848cdbfd1b49600574e7be312ec55731a9e3", - "bytes": 40544, - "width": 1280, - "height": 720 - }, - { - "name": "ownerdrawn-03-font.png", - "engine": "chromium", - "sha256": "659fbe9e53c69209ce50a5463716848cdbfd1b49600574e7be312ec55731a9e3", - "bytes": 40544, - "width": 1280, - "height": 720 - }, - { - "name": "ownerdrawn-04-icon.png", - "engine": "chromium", - "sha256": "659fbe9e53c69209ce50a5463716848cdbfd1b49600574e7be312ec55731a9e3", - "bytes": 40544, - "width": 1280, - "height": 720 - }, - { - "name": "ownerdrawn-05-log.png", - "engine": "chromium", - "sha256": "659fbe9e53c69209ce50a5463716848cdbfd1b49600574e7be312ec55731a9e3", - "bytes": 40544, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-context-menu.png", - "engine": "chromium", - "sha256": "214e5854b0176c7d0789557bab76b607590a122a9cca5005dcd4f99a05e54193", - "bytes": 112833, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-context-submenu.png", - "engine": "chromium", - "sha256": "2a1e9e103070b2a764d36cc80a8c06ac7fd5f27f996cc04ebc1f084ffb8ca13b", - "bytes": 125038, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-dark-mode-loaded.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-00-before-tool-click.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-01-after-click.png", - "engine": "chromium", - "sha256": "a233f32bd68aa497c337f5fc53da02c8920fddca6854383156ad962f65477d52", - "bytes": 107573, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-02-after-drawing.png", - "engine": "chromium", - "sha256": "0d45d478dbaeb79e42618a09e9ea3957cba8aa6693037e31e23e2f028c2f5f75", - "bytes": 113101, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-loaded-css.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-loaded.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-move-00-before.png", - "engine": "chromium", - "sha256": "2ce68756b768af4d1e5e8446f839f9f90ccc458dd3db48919e1efdde0cf66d03", - "bytes": 108150, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-move-01-after.png", - "engine": "chromium", - "sha256": "e43612a8ac4c11917b0c8160259d09ec2a5bab640207e70713c3526f3b9e2cec", - "bytes": 111441, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-sidebar-scrollbar-dragged.png", - "engine": "chromium", - "sha256": "c14de3d3ff82a6eace168a4d24081fd71d1ede0b76ae4c8b33b057d41ea23e58", - "bytes": 104311, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-sidebar-scrollbar.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "pickers-01-loaded.png", - "engine": "chromium", - "sha256": "36311c6e048cb7f367b7478f6bb61d8fa83f86be4c087d4884790e4efc21665c", - "bytes": 44419, - "width": 1280, - "height": 720 - }, - { - "name": "pickers-02-colors.png", - "engine": "chromium", - "sha256": "36311c6e048cb7f367b7478f6bb61d8fa83f86be4c087d4884790e4efc21665c", - "bytes": 44419, - "width": 1280, - "height": 720 - }, - { - "name": "pickers-03-font.png", - "engine": "chromium", - "sha256": "36311c6e048cb7f367b7478f6bb61d8fa83f86be4c087d4884790e4efc21665c", - "bytes": 44419, - "width": 1280, - "height": 720 - }, - { - "name": "pickers-04-preview.png", - "engine": "chromium", - "sha256": "36311c6e048cb7f367b7478f6bb61d8fa83f86be4c087d4884790e4efc21665c", - "bytes": 44419, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-01-loaded.png", - "engine": "chromium", - "sha256": "38027e75473ad505f8675fe52f1c7d4e85a42e59b68ba6ea0d93964ed7db6d86", - "bytes": 61645, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-02-no-wizard.png", - "engine": "chromium", - "sha256": "38027e75473ad505f8675fe52f1c7d4e85a42e59b68ba6ea0d93964ed7db6d86", - "bytes": 61645, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-03-file-menu.png", - "engine": "chromium", - "sha256": "d79ddea66737a04a71e136cfaa8c955c70d22fdc7ef5b208901c546c137f7197", - "bytes": 36383, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-04-save-as-dialog.png", - "engine": "chromium", - "sha256": "8d1a6794ee7d3cd02a1b8fbe4de596f67024824ffe4c12188103da58c34bc1b7", - "bytes": 41910, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-04b-after-enter.png", - "engine": "chromium", - "sha256": "8d1a6794ee7d3cd02a1b8fbe4de596f67024824ffe4c12188103da58c34bc1b7", - "bytes": 41910, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-context-menu.png", - "engine": "chromium", - "sha256": "41d1ed13f42b76530f319f5cd76550f6a2e6ee20c40200918c30af97f0de5b7d", - "bytes": 40995, - "width": 1280, - "height": 540 - }, - { - "name": "pl_editor-load-rendered.png", - "engine": "chromium", - "sha256": "949caa8b28d50a54bf23e81b30587aea97e8fb01173110169d69ac86eb9e1931", - "bytes": 33734, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-scrollbar-dragged.png", - "engine": "chromium", - "sha256": "9fe5987a042c4c3514512dc9d5490764a9ed9ca51390517d39396da9b92bbdf7", - "bytes": 40589, - "width": 1203, - "height": 460 - }, - { - "name": "pl_editor-scrollbar.png", - "engine": "chromium", - "sha256": "f4b0a4b2f84de265b37c67576d2900e1f18a010855a7439698d22dcfd1928948", - "bytes": 40693, - "width": 1203, - "height": 507 - }, - { - "name": "popup-01-loaded.png", - "engine": "chromium", - "sha256": "59dd326665bca9781534a591b5a440924a7ce5a73b2e5b2db991840ca49d1044", - "bytes": 39818, - "width": 1280, - "height": 720 - }, - { - "name": "popup-02-status.png", - "engine": "chromium", - "sha256": "59dd326665bca9781534a591b5a440924a7ce5a73b2e5b2db991840ca49d1044", - "bytes": 39818, - "width": 1280, - "height": 720 - }, - { - "name": "popup-03-palette-dismissed.png", - "engine": "chromium", - "sha256": "17057d10f26dc7aa781f416260a1864c160fe6760459ffd703fe2d4489ff089a", - "bytes": 42654, - "width": 1280, - "height": 720 - }, - { - "name": "popup-03-palette-open.png", - "engine": "chromium", - "sha256": "d2966206503fed8ee536beb7ac33e8a8119788d30b6db37a5100de1c37c3c929", - "bytes": 40990, - "width": 1280, - "height": 720 - }, - { - "name": "popup-04-color.png", - "engine": "chromium", - "sha256": "59dd326665bca9781534a591b5a440924a7ce5a73b2e5b2db991840ca49d1044", - "bytes": 39818, - "width": 1280, - "height": 720 - }, - { - "name": "popup-05-positioning.png", - "engine": "chromium", - "sha256": "59dd326665bca9781534a591b5a440924a7ce5a73b2e5b2db991840ca49d1044", - "bytes": 39818, - "width": 1280, - "height": 720 - }, - { - "name": "popup-06-log.png", - "engine": "chromium", - "sha256": "59dd326665bca9781534a591b5a440924a7ce5a73b2e5b2db991840ca49d1044", - "bytes": 39818, - "width": 1280, - "height": 720 - }, - { - "name": "print-01-loaded.png", - "engine": "chromium", - "sha256": "bf1d0278802d2cbb940c330a435fcfa7d6c1ad5ebd30d19d2718e190a45c4dcb", - "bytes": 34760, - "width": 1280, - "height": 720 - }, - { - "name": "print-02-preview-panel.png", - "engine": "chromium", - "sha256": "bf1d0278802d2cbb940c330a435fcfa7d6c1ad5ebd30d19d2718e190a45c4dcb", - "bytes": 34760, - "width": 1280, - "height": 720 - }, - { - "name": "print-03-browser-print-clicked.png", - "engine": "chromium", - "sha256": "f606ae66475daf3042c9a947b72660c328b6135d2494088a09fe8706ecd23317", - "bytes": 42574, - "width": 1280, - "height": 720 - }, - { - "name": "print-04-preview-clicked.png", - "engine": "chromium", - "sha256": "ead933f3b167c8fbcfd2aebbcdb9bfa81b96f81f8617142180ff5c2ab6c8c1c1", - "bytes": 38660, - "width": 1280, - "height": 720 - }, - { - "name": "print-05-page-setup-clicked.png", - "engine": "chromium", - "sha256": "f30638d525e4ca602463d44565d47495417f3803c6794592f190163ce9dc49fd", - "bytes": 37691, - "width": 1280, - "height": 720 - }, - { - "name": "print-06-print-clicked.png", - "engine": "chromium", - "sha256": "b9eb6ee63236cef151812cccae79fd8428f016ac2ba39f276f398f9a56b2eb2c", - "bytes": 36796, - "width": 1280, - "height": 720 - }, - { - "name": "print-07-callbacks.png", - "engine": "chromium", - "sha256": "ead933f3b167c8fbcfd2aebbcdb9bfa81b96f81f8617142180ff5c2ab6c8c1c1", - "bytes": 38660, - "width": 1280, - "height": 720 - }, - { - "name": "print-08-all-buttons.png", - "engine": "chromium", - "sha256": "65af278e5830afc1ce5be1e5f2a3c05bf64e95b9bca41c116512d5bd781d478b", - "bytes": 51497, - "width": 1280, - "height": 720 - }, - { - "name": "printpreview-01-loaded.png", - "engine": "chromium", - "sha256": "159a59aa3340ccf15f372176d48d69f73c14be5c6026f34e2c67b8c64e9f6f68", - "bytes": 38659, - "width": 1280, - "height": 720 - }, - { - "name": "printpreview-02-preview-area.png", - "engine": "chromium", - "sha256": "159a59aa3340ccf15f372176d48d69f73c14be5c6026f34e2c67b8c64e9f6f68", - "bytes": 38659, - "width": 1280, - "height": 720 - }, - { - "name": "printpreview-03-preview-frame.png", - "engine": "chromium", - "sha256": "9f7cfd01ed6223bc1753e5b834534a87ee0a98a24d607bb1db5a6ea3cc4e56b8", - "bytes": 61075, - "width": 1280, - "height": 1332 - }, - { - "name": "printpreview-04-page-setup.png", - "engine": "chromium", - "sha256": "b164f84fdcaa6045bd3a3afe7313d9e80037a59aef1ed65dff949fbb0a7309c5", - "bytes": 60795, - "width": 1280, - "height": 1212 - }, - { - "name": "printpreview-05-settings.png", - "engine": "chromium", - "sha256": "159a59aa3340ccf15f372176d48d69f73c14be5c6026f34e2c67b8c64e9f6f68", - "bytes": 38659, - "width": 1280, - "height": 720 - }, - { - "name": "probe-00-after-wizard.png", - "engine": "chromium", - "sha256": "4ee503ba398bc613a0d243ce0b8ecae01030e8caa348d924acee1f2a125072d5", - "bytes": 106098, - "width": 1280, - "height": 720 - }, - { - "name": "probe-01-file-menu-open.png", - "engine": "chromium", - "sha256": "6d0ec2529f8d7df1071661860bfeeb8c41d296e3c2701a030a96100d283d0c93", - "bytes": 115349, - "width": 1280, - "height": 720 - }, - { - "name": "probe-02-after-open-click.png", - "engine": "chromium", - "sha256": "5b113fbe9f194eda45cea726ef53dbf721b78040f3ddfe4c576ef3c197d6f2ea", - "bytes": 119819, - "width": 1280, - "height": 720 - }, - { - "name": "probe-03-late.png", - "engine": "chromium", - "sha256": "5b113fbe9f194eda45cea726ef53dbf721b78040f3ddfe4c576ef3c197d6f2ea", - "bytes": 119819, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-00-board-open.png", - "engine": "chromium", - "sha256": "548d15e28d12698e01911de97acabeaa111df5abb23a4c6218ebdb4d0285fc23", - "bytes": 95576, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-01-update-dialog.png", - "engine": "chromium", - "sha256": "8955464503634edf05a335de3d7db9b6b4508d47508af8b371a0363a40e03a97", - "bytes": 130070, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-02-resync-first-R777.png", - "engine": "chromium", - "sha256": "e6d94412b4641862d28d75e662bf32ea7f9f3091656581e65527a4517c1788bb", - "bytes": 140967, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-03-resync-second-R888.png", - "engine": "chromium", - "sha256": "bd2041a98b1608d2c9e5fe31a973349ee69b049c7c9fc27dc80a78d8c74df81d", - "bytes": 141431, - "width": 1280, - "height": 720 - }, - { - "name": "propgrid-01-loaded.png", - "engine": "chromium", - "sha256": "06c56805cf4b5c1c0c47412d8dabdc5cfacd6b4b234d9e94cc2fbb613169ea19", - "bytes": 51560, - "width": 1280, - "height": 720 - }, - { - "name": "propgrid-02-categories.png", - "engine": "chromium", - "sha256": "06c56805cf4b5c1c0c47412d8dabdc5cfacd6b4b234d9e94cc2fbb613169ea19", - "bytes": 51560, - "width": 1280, - "height": 720 - }, - { - "name": "propgrid-03-selected.png", - "engine": "chromium", - "sha256": "37a31618cbe2ade6e0010412dc2dee376a5a41add0cf1425822510426d949b0a", - "bytes": 53900, - "width": 1280, - "height": 720 - }, - { - "name": "propgrid-04-manager.png", - "engine": "chromium", - "sha256": "06c56805cf4b5c1c0c47412d8dabdc5cfacd6b4b234d9e94cc2fbb613169ea19", - "bytes": 51560, - "width": 1280, - "height": 720 - }, - { - "name": "radiogroups-01-loaded.png", - "engine": "chromium", - "sha256": "ef78bd25fb753742d37b5dce3e3ab73486aef5e993168a7d973564be964ae0b9", - "bytes": 38359, - "width": 1280, - "height": 720 - }, - { - "name": "radiogroups-02-selections.png", - "engine": "chromium", - "sha256": "86704e44570db2289947df6512650d38ef3ccdd8634bb0c0c849bc0e89c999e7", - "bytes": 38377, - "width": 1280, - "height": 720 - }, - { - "name": "regions.png", - "engine": "chromium", - "sha256": "0660c0d0732fc5a0d323077dafb904358bff290a73f82f47de472bb701a65b52", - "bytes": 63641, - "width": 1280, - "height": 720 - }, - { - "name": "scrollbar-01-loaded.png", - "engine": "chromium", - "sha256": "9fb3de835c8886b8df5883047bbf0b91c47f7f9763eed01eb4cc255f20832c88", - "bytes": 12039, - "width": 1280, - "height": 720 - }, - { - "name": "scrollbar-02-dragged.png", - "engine": "chromium", - "sha256": "7425ca16d7277eaa7862b10085a30bfea53cc7759758e17c082ccdea2cb90f9a", - "bytes": 6857, - "width": 1280, - "height": 720 - }, - { - "name": "searchctrl-01-visible.png", - "engine": "chromium", - "sha256": "bc4ca83b02969d1b2f10c170c6ae2e4e361ac457ca85008233f70c9b31a1e592", - "bytes": 38535, - "width": 1280, - "height": 720 - }, - { - "name": "searchctrl-02-typing.png", - "engine": "chromium", - "sha256": "69ea8f9026bfaa4cf220b2cc99b939010e4bc2ef193d4020654087f0a40f27ac", - "bytes": 38692, - "width": 1280, - "height": 720 - }, - { - "name": "searchctrl-03-after-enter.png", - "engine": "chromium", - "sha256": "69ea8f9026bfaa4cf220b2cc99b939010e4bc2ef193d4020654087f0a40f27ac", - "bytes": 38692, - "width": 1280, - "height": 720 - }, - { - "name": "selectheight-01-loaded.png", - "engine": "chromium", - "sha256": "7c4e2dd1d04bae6c077a0ddcd7830f89e37bfd3396ef07e0d64d18aa1eb73877", - "bytes": 15659, - "width": 1280, - "height": 720 - }, - { - "name": "selectheight-02-result.png", - "engine": "chromium", - "sha256": "7c4e2dd1d04bae6c077a0ddcd7830f89e37bfd3396ef07e0d64d18aa1eb73877", - "bytes": 15659, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-01-loaded.png", - "engine": "chromium", - "sha256": "1166856ca635c04401e3335e6a30fb463132e71255f6aeb3f40208281952d2fb", - "bytes": 50334, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-02-treebook-initial.png", - "engine": "chromium", - "sha256": "1166856ca635c04401e3335e6a30fb463132e71255f6aeb3f40208281952d2fb", - "bytes": 50334, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-03-treebook-subpage.png", - "engine": "chromium", - "sha256": "71d53fd2c62f719167b9854de0dcb5f815ff2786e4a13da35cbd0ce04f02cc47", - "bytes": 53772, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-04-treebook-expand.png", - "engine": "chromium", - "sha256": "fcd5e2e85af9212b748da70600083ea51e832ca08f09624c16681d1f10c15599", - "bytes": 54063, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-05-bitmapcombo-dropdown.png", - "engine": "chromium", - "sha256": "fd42e0a04c2777f9c854aa4d0be66c5bd3da05a6dbd9f35cd02bde2e09b2d009", - "bytes": 54818, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-06-bitmapcombo-select.png", - "engine": "chromium", - "sha256": "28c2aba2d6724f4f4f0b4bc2ac1f4f3c1ccdca3a7e6d7f88e28a3cf75414c4b2", - "bytes": 54799, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-07-rearrange-list.png", - "engine": "chromium", - "sha256": "1166856ca635c04401e3335e6a30fb463132e71255f6aeb3f40208281952d2fb", - "bytes": 50334, - "width": 1280, - "height": 720 - }, - { - "name": "specialized-08-get-order.png", - "engine": "chromium", - "sha256": "062c704171c91afdb96c6904ecade92563b35e308c2706aed974b7cd3b243eed", - "bytes": 57932, - "width": 1280, - "height": 720 - }, - { - "name": "spinctrl-01-visible.png", - "engine": "chromium", - "sha256": "bc4ca83b02969d1b2f10c170c6ae2e4e361ac457ca85008233f70c9b31a1e592", - "bytes": 38535, - "width": 1280, - "height": 720 - }, - { - "name": "spinctrl-02-after-click.png", - "engine": "chromium", - "sha256": "bc4ca83b02969d1b2f10c170c6ae2e4e361ac457ca85008233f70c9b31a1e592", - "bytes": 38535, - "width": 1280, - "height": 720 - }, - { - "name": "stc-01-loaded.png", - "engine": "chromium", - "sha256": "7bfb1550bd13b02bf3b4cc8fdbabe999111ffdda9de1e6b3d46b8beff49fd574", - "bytes": 58176, - "width": 1280, - "height": 720 - }, - { - "name": "stc-02-python-default.png", - "engine": "chromium", - "sha256": "7bfb1550bd13b02bf3b4cc8fdbabe999111ffdda9de1e6b3d46b8beff49fd574", - "bytes": 58176, - "width": 1280, - "height": 720 - }, - { - "name": "stc-03-drc-mode.png", - "engine": "chromium", - "sha256": "284d3097cc6d148a3aba8a590acea1168a47104d52f25f9e6c34ebf2ca369f42", - "bytes": 63365, - "width": 1280, - "height": 720 - }, - { - "name": "stc-04-plain-mode.png", - "engine": "chromium", - "sha256": "eccdc3cbf7b2756eec5a91a0906c1d6a6e67310fa26f40fe93b18f6a180e66f4", - "bytes": 58866, - "width": 1280, - "height": 720 - }, - { - "name": "stc-05-insert-sample.png", - "engine": "chromium", - "sha256": "0c485809e8d547f6e4b62f176446510d2f88fcec26f2b923b812d1ed06fed56c", - "bytes": 64134, - "width": 1280, - "height": 720 - }, - { - "name": "stc-06-cleared.png", - "engine": "chromium", - "sha256": "32058ff074d1013f873cd7a18373db83aa302b6194967dab744394259da257d1", - "bytes": 32610, - "width": 1280, - "height": 720 - }, - { - "name": "stc-07-line-numbers-toggle.png", - "engine": "chromium", - "sha256": "d23bd16d122f550b08234009accba3adaeeecec78af1d03f259d1cdc1d40b2b7", - "bytes": 57502, - "width": 1280, - "height": 720 - }, - { - "name": "stc-08-folded.png", - "engine": "chromium", - "sha256": "23161332c6a4606915be386df861f2e37da57b4ec16283776a5f916ce96749ff", - "bytes": 60106, - "width": 1280, - "height": 720 - }, - { - "name": "stc-09-typed.png", - "engine": "chromium", - "sha256": "dfffbe8f0318ef6734342931db00f6b7e8e47f3852f1a8223638d977475d6c76", - "bytes": 44131, - "width": 1280, - "height": 720 - }, - { - "name": "stc-10a-python.png", - "engine": "chromium", - "sha256": "7bfb1550bd13b02bf3b4cc8fdbabe999111ffdda9de1e6b3d46b8beff49fd574", - "bytes": 58176, - "width": 1280, - "height": 720 - }, - { - "name": "stc-10b-drc.png", - "engine": "chromium", - "sha256": "284d3097cc6d148a3aba8a590acea1168a47104d52f25f9e6c34ebf2ca369f42", - "bytes": 63365, - "width": 1280, - "height": 720 - }, - { - "name": "stc-10c-python-again.png", - "engine": "chromium", - "sha256": "21045335a797ea897fb5a11235cc2561efc7144294213c2cc6d8d210290877bd", - "bytes": 63193, - "width": 1280, - "height": 720 - }, - { - "name": "symbol_editor-01-loaded.png", - "engine": "chromium", - "sha256": "91bd83a6ce37352cbfb6e63e84cdb05e308ee2b51ed81654210127a7400778df", - "bytes": 42706, - "width": 1280, - "height": 720 - }, - { - "name": "symbol_editor-02-metrics.png", - "engine": "chromium", - "sha256": "91bd83a6ce37352cbfb6e63e84cdb05e308ee2b51ed81654210127a7400778df", - "bytes": 42706, - "width": 1280, - "height": 720 - }, - { - "name": "textdecor.png", - "engine": "chromium", - "sha256": "1299f4d5b9720d7e91365f061cf2e49e8a0d7aba4091ae40de7e6c6197451725", - "bytes": 70599, - "width": 1280, - "height": 720 - }, - { - "name": "threadpool-01-loaded.png", - "engine": "chromium", - "sha256": "7a43c8836fd5fef111e6dda623c3c78b7419965cf9f5a69cd44b2a31ec374bbe", - "bytes": 31812, - "width": 1280, - "height": 720 - }, - { - "name": "timer-01-loaded.png", - "engine": "chromium", - "sha256": "932d43cc87473c7238e3ce6d54411ce3c53524e2204f7ca0368b1e3e418987da", - "bytes": 32107, - "width": 1280, - "height": 720 - }, - { - "name": "timer-02-started.png", - "engine": "chromium", - "sha256": "9438fd8cc61f22cf4a8b3d3ac36be4eb18ccc9078274008cd351f54ca920278d", - "bytes": 35834, - "width": 1280, - "height": 720 - }, - { - "name": "timer-03-ticked.png", - "engine": "chromium", - "sha256": "4db6ac851ce90b9ea5aed89c335a675c071978a3a7a1fe74c16f1512cc1e90e3", - "bytes": 35743, - "width": 1280, - "height": 720 - }, - { - "name": "timer-04-stopped.png", - "engine": "chromium", - "sha256": "d1aaa5ffa8eb9f6b68a0bab9b918b16124b57573f5c1b6643a1f8e2b434f54a7", - "bytes": 36393, - "width": 1280, - "height": 720 - }, - { - "name": "timer-05-fast-started.png", - "engine": "chromium", - "sha256": "53e7586d30edaa41b781eeca592cf618aae86079fe5c48d23c9de2d6af6a4de0", - "bytes": 35583, - "width": 1280, - "height": 720 - }, - { - "name": "timer-06-fast-running.png", - "engine": "chromium", - "sha256": "b034232114f802580fbce83b90d1f82491fd3c9091c9020f194a8ebddf238490", - "bytes": 35632, - "width": 1280, - "height": 720 - }, - { - "name": "timer-07-fast-stopped.png", - "engine": "chromium", - "sha256": "9d56d0b2a74cb6ad861ce34f3f330f101299e19bf247a333c28ea8ed121a28aa", - "bytes": 36003, - "width": 1280, - "height": 720 - }, - { - "name": "timer-08-reset.png", - "engine": "chromium", - "sha256": "2e787910fd221e909fb5b078b63713a6bb1062754d5934dbbec881b6500a3bf8", - "bytes": 34976, - "width": 1280, - "height": 720 - }, - { - "name": "toolbar-01-loaded.png", - "engine": "chromium", - "sha256": "edd9f1eecd9d9857bfea90a6cab9f308ab79f885f09fb2d02af4f3413ace82e5", - "bytes": 29039, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-02-buttons.png", - "engine": "chromium", - "sha256": "edd9f1eecd9d9857bfea90a6cab9f308ab79f885f09fb2d02af4f3413ace82e5", - "bytes": 29039, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-03-new-clicked.png", - "engine": "chromium", - "sha256": "e54b6dfc44f1d27ae3e889f7d05ae6b49bffbb8b8fea00d0b13f71ec6e01232d", - "bytes": 26639, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-04-zoom.png", - "engine": "chromium", - "sha256": "7aaa32d4a959ee7ce1be79f832706b46e825b1491b4f1ab068f923543151a156", - "bytes": 28654, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-05-toggle-on.png", - "engine": "chromium", - "sha256": "9eb1ef157f7e2a9f15a796611b60468601744e796b478a0266f47379f00719be", - "bytes": 26479, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-06-toggle-off.png", - "engine": "chromium", - "sha256": "98a645d91d09ef1cc8fcd950476e043aa5f703a94924a9fa80d82fcbcc65535e", - "bytes": 28138, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-07-statusbar.png", - "engine": "chromium", - "sha256": "edd9f1eecd9d9857bfea90a6cab9f308ab79f885f09fb2d02af4f3413ace82e5", - "bytes": 29039, - "width": 1280, - "height": 721 - }, - { - "name": "toolbar-08-all-buttons.png", - "engine": "chromium", - "sha256": "1924b802ab54eb08383b7ed05f927c3974d9efcd8654a34c99b6d6c672e7f37f", - "bytes": 35034, - "width": 1280, - "height": 721 - }, - { - "name": "tree-01-loaded.png", - "engine": "chromium", - "sha256": "8f0462a6d7497ba7a0b6ed66409a290968fe4737381f9a01681f026156e76f5c", - "bytes": 36875, - "width": 1280, - "height": 720 - }, - { - "name": "tree-02-hierarchy.png", - "engine": "chromium", - "sha256": "8f0462a6d7497ba7a0b6ed66409a290968fe4737381f9a01681f026156e76f5c", - "bytes": 36875, - "width": 1280, - "height": 720 - }, - { - "name": "tree-03-selected.png", - "engine": "chromium", - "sha256": "32c87032d668964d9c4fef967328bdc845eedb07b77a81e53ab826cdffdd6234", - "bytes": 39411, - "width": 1280, - "height": 720 - }, - { - "name": "tree-04-expanded.png", - "engine": "chromium", - "sha256": "46df128a5a60c11e01e0bcd3dd04681c35f27adf0923f159aa074717621aac16", - "bytes": 63260, - "width": 1280, - "height": 720 - }, - { - "name": "tree-05-collapsed.png", - "engine": "chromium", - "sha256": "56af815f7d698dd57d38709cd22622eec7ba8384db57af0fe8322ac3f30f8477", - "bytes": 36692, - "width": 1280, - "height": 720 - }, - { - "name": "tree-06-added.png", - "engine": "chromium", - "sha256": "6bb014fc49a1a637144f44f95d7f7e012fe9d2acad876b4df794fbf282d741f1", - "bytes": 44180, - "width": 1280, - "height": 720 - }, - { - "name": "tree-07-deleted.png", - "engine": "chromium", - "sha256": "2ba16f8851c81ef68ba3f48fb21559d59450680fb7d242378514762ee65183f3", - "bytes": 42441, - "width": 1280, - "height": 720 - }, - { - "name": "validators-01-loaded.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "validators-02-text.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "validators-03-integer.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "validators-04-float.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "validators-05-netname.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "validators-06-button.png", - "engine": "chromium", - "sha256": "a82d021dc5f6f678643499a732c55c39558c921ded646bcb13e80fd8d5370ba6", - "bytes": 47554, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-01-loaded.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-02-filesystem.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-03-threading.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-04-fonts.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-05-clipboard.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-06-memory.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-07-runall.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "wasmedge-08-log.png", - "engine": "chromium", - "sha256": "8d4aa3b94e283e38a389f46c12d3e58b2af1406a1e6b6bfe44c212be14bf4555", - "bytes": 66609, - "width": 1280, - "height": 720 - }, - { - "name": "web-calculator.png", - "engine": "chromium", - "sha256": "9e1b8512ba5e5eb3bbce969891f84fcb40514fd5a052f88b9e162a8109dfc3db", - "bytes": 74004, - "width": 1280, - "height": 720 - }, - { - "name": "web-chrome-hidden.png", - "engine": "chromium", - "sha256": "a75246340fb00484885cfa49e73504432f7a587cd9a2447c42a3f1f289b0fc3e", - "bytes": 82394, - "width": 1280, - "height": 720 - }, - { - "name": "web-chrome-restored.png", - "engine": "chromium", - "sha256": "2cb8d18e415ace9d35938583c1fe2cee8b3143fa22ac006b16900286a58cc94f", - "bytes": 187149, - "width": 1280, - "height": 720 - }, - { - "name": "web-eeschema.png", - "engine": "chromium", - "sha256": "1be20a087a6bdf8761b4bee9a7499c6b4d209b77f1732ee0a20080c9f182ff04", - "bytes": 13128, - "width": 1280, - "height": 720 - }, - { - "name": "web-footprint_editor.png", - "engine": "chromium", - "sha256": "858fc1b7f9c879030da3eefd0d167618afee1bebb71f4de4c6d929aeebdbe5da", - "bytes": 73707, - "width": 1280, - "height": 720 - }, - { - "name": "web-gerbview.png", - "engine": "chromium", - "sha256": "b347f63f0fdb68851e65551b14791a6621fa8ef93e0a0ecb9975335ad31d17a1", - "bytes": 44869, - "width": 1280, - "height": 720 - }, - { - "name": "web-pcbnew.png", - "engine": "chromium", - "sha256": "339b4fb38e14d81b49527bccebd8e662def076ce03316f33c907f65ba78967c7", - "bytes": 13098, - "width": 1280, - "height": 720 - }, - { - "name": "web-pl_editor.png", - "engine": "chromium", - "sha256": "deccaf9fc3ee77480d930856ac00186644474a6cb8bc18812582f9783d7e4ae2", - "bytes": 12528, - "width": 1280, - "height": 720 - }, - { - "name": "web-read-only-viewer.png", - "engine": "chromium", - "sha256": "8054e0b4ece2aeb4a0a0ab8db811b48210aee2cd3c894c213286002d51b3324d", - "bytes": 113244, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-missing-pcb-created.png", - "engine": "chromium", - "sha256": "b7ca2cd9a46e80337c18717b44a0a5be349d6b163a6ffc78d9284ee2e132bb40", - "bytes": 103962, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-pcb-to-sch.png", - "engine": "chromium", - "sha256": "05122a1eb1881b5826e61a102b45dc1fa1fbb8a6d723e34d8c2e9a73a7676f8e", - "bytes": 77659, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-sch-to-pcb.png", - "engine": "chromium", - "sha256": "e9eef7e68ff7bacb668f3791e327c15417d90845f2c10729209c458b12a32ea8", - "bytes": 197181, - "width": 1280, - "height": 720 - }, - { - "name": "web-symbol_editor.png", - "engine": "chromium", - "sha256": "b770263b57d9743deb6aaf048f938f1d19deec8e42b8318a366f6264c2437468", - "bytes": 45480, - "width": 1280, - "height": 720 - }, - { - "name": "wizard-01-loaded.png", - "engine": "chromium", - "sha256": "33ba0f7d0fa58136219b9cf6a58618a9423203cbde525477894b3044bb8034cd", - "bytes": 23601, - "width": 1280, - "height": 720 - }, - { - "name": "wizard-02-launch.png", - "engine": "chromium", - "sha256": "1f83c1bd8f87b8d63c4078b8e0925a30667294fb04f056353cb16de329caa70b", - "bytes": 46963, - "width": 1280, - "height": 1264 - }, - { - "name": "wizard-03-next-page.png", - "engine": "chromium", - "sha256": "83a9466d9bf194b9a7396325187115933385a1e3962fc6fe1053c582bf6a04af", - "bytes": 46932, - "width": 1280, - "height": 1264 - }, - { - "name": "wizard-04-back-page.png", - "engine": "chromium", - "sha256": "83a9466d9bf194b9a7396325187115933385a1e3962fc6fe1053c582bf6a04af", - "bytes": 46932, - "width": 1280, - "height": 1264 - }, - { - "name": "wizard-05-cancel.png", - "engine": "chromium", - "sha256": "9096123effd77e16e7705ccb54e928cb1fbb690408b8b8c599fcabd4e0ad6f46", - "bytes": 47118, - "width": 1280, - "height": 1264 - }, - { - "name": "wxgrid-01-tab.png", - "engine": "chromium", - "sha256": "bc4ca83b02969d1b2f10c170c6ae2e4e361ac457ca85008233f70c9b31a1e592", - "bytes": 38535, - "width": 1280, - "height": 720 - }, - { - "name": "wxgrid-02-selection.png", - "engine": "chromium", - "sha256": "acd5c14ec295f7f4a7a85076c627005bc5634f730a448e4fffe0d326ab7a3cce", - "bytes": 38557, - "width": 1280, - "height": 720 - }, - { - "name": "wxgrid-03-editing.png", - "engine": "chromium", - "sha256": "acd5c14ec295f7f4a7a85076c627005bc5634f730a448e4fffe0d326ab7a3cce", - "bytes": 38557, - "width": 1280, - "height": 720 - }, - { - "name": "wxgrid-controls.png", - "engine": "chromium", - "sha256": "8f237999b01378a755bb2728002f516fae625d7fd01caf8b75f048a775564b98", - "bytes": 32415, - "width": 1280, - "height": 720 - }, - { - "name": "wxgrid-dedicated-page.png", - "engine": "chromium", - "sha256": "8f237999b01378a755bb2728002f516fae625d7fd01caf8b75f048a775564b98", - "bytes": 32415, - "width": 1280, - "height": 720 - }, - { - "name": "xface-preferences.png", - "engine": "chromium", - "sha256": "5c7c57d58de052f1c632296e46cb2953679a13918968ab18cf8bc86635366ba2", - "bytes": 149220, - "width": 1280, - "height": 720 - }, - { - "name": "xml-01-loaded.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "xml-02-input.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "xml-03-parse.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "xml-04-traverse.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "xml-05-create.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "xml-06-results.png", - "engine": "chromium", - "sha256": "542ae05e3aae73ac18e8c6eac2c4f02ffa27d0d897da0886010bdde8ad1f9d47", - "bytes": 49811, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-00-board-loaded.png", - "engine": "firefox", - "sha256": "7770ead25e3136a0fe9ecc3515c7a57fcbfde42d2ee9bf66965d1a80d0e2fb5f", - "bytes": 304662, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-models-pic_programmer-render.png", - "engine": "firefox", - "sha256": "13a3b995487f08ddcc6c1a02296453c31851ed553ed13bafba405690ccd1ce6c", - "bytes": 66250, - "width": 1084, - "height": 623 - }, - { - "name": "3d-viewer-models-pic_programmer.png", - "engine": "firefox", - "sha256": "699ae900cc1611a9bf1638f346fb2af4aee082d7562feb741ead84b8826da78b", - "bytes": 157000, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-pic_programmer-render.png", - "engine": "firefox", - "sha256": "6fb5fc36ad59a4d993984ba27f9cc080bba453c5470b789806f9a92d9fb8abc6", - "bytes": 64238, - "width": 1084, - "height": 623 - }, - { - "name": "3d-viewer-pic_programmer.png", - "engine": "firefox", - "sha256": "d29f26c34bad9fde6fdbf9b378c42f049a0b32eb196b5e76232e0abb802c8c20", - "bytes": 153293, - "width": 1280, - "height": 720 - }, - { - "name": "3d-viewer-titlebar.png", - "engine": "firefox", - "sha256": "cc94b51471f6238b5befbbb65648351579208ab3328823c3a4cad5ca9c8d7911", - "bytes": 153854, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-00-layers.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-01-objects.png", - "engine": "firefox", - "sha256": "743903e05927cefc8247871eba298166cc1df4f2642d0881424f0e4955352f50", - "bytes": 152609, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-02-nets.png", - "engine": "firefox", - "sha256": "db8ee4fe7e58e954fbf994ce97d83ac81fec3f4aa38ac742f49f7e0f836477c8", - "bytes": 126338, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-03-layers-again.png", - "engine": "firefox", - "sha256": "075de1dfc3786d2b721eff089de55161524cef578254920134278c82e254a345", - "bytes": 152105, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-10-layers-scrolled.png", - "engine": "firefox", - "sha256": "1ab1fa82acc129c9d990f7ebc6ec446948966a194966a23fd11b14b9bcc1d454", - "bytes": 151863, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-11-layers-scrolled-back.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "appearance-20-objects-scrolled.png", - "engine": "firefox", - "sha256": "ddd293ec09852422d4d4fcd5c52d06315845734222da91953ba677acf94d1496", - "bytes": 154490, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-before-switch.png", - "engine": "firefox", - "sha256": "438152d6d9fa39761f32a8df342a7db27175d05124d6fb8141f73a8c1cc3291f", - "bytes": 109471, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-color-code.png", - "engine": "firefox", - "sha256": "c205d9fbb8d8e9c0174816fca65056924f3b4d33fb6611fc9dda280686981727", - "bytes": 108135, - "width": 1280, - "height": 720 - }, - { - "name": "calculator-loaded.png", - "engine": "firefox", - "sha256": "438152d6d9fa39761f32a8df342a7db27175d05124d6fb8141f73a8c1cc3291f", - "bytes": 109471, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-01-loaded.png", - "engine": "firefox", - "sha256": "49b10ce00fcbef9a977ed0bdef1fb7a8fef32e9412f55e88a870d538e88d106f", - "bytes": 143828, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-02-summary.png", - "engine": "firefox", - "sha256": "49b10ce00fcbef9a977ed0bdef1fb7a8fef32e9412f55e88a870d538e88d106f", - "bytes": 143828, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-nested-01-loaded.png", - "engine": "firefox", - "sha256": "3c5b72c873bfc2a2ddc350746535317b364e37cfcad0fc5ade498d5df522ea87", - "bytes": 111212, - "width": 1280, - "height": 720 - }, - { - "name": "coroutine-nested-02-summary.png", - "engine": "firefox", - "sha256": "3c5b72c873bfc2a2ddc350746535317b364e37cfcad0fc5ade498d5df522ea87", - "bytes": 111212, - "width": 1280, - "height": 720 - }, - { - "name": "eefpsel-01-boot.png", - "engine": "firefox", - "sha256": "07200fdfd7277f1798bc0ed06cc7615255fbc657d77d1fec1005bd3f3456f46a", - "bytes": 99057, - "width": 1280, - "height": 720 - }, - { - "name": "eefpsel-02-chooser.png", - "engine": "firefox", - "sha256": "0fb4936a14a7c5958ff671d33f58c36b737721a5ad7fee60eb32751ef72f539c", - "bytes": 117983, - "width": 1280, - "height": 720 - }, - { - "name": "eefpsel-03-selected.png", - "engine": "firefox", - "sha256": "bd2a388b1715e0cee622687707b1be7b63a862951b5370e7eafd88b42c997afb", - "bytes": 138608, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-00-small.png", - "engine": "firefox", - "sha256": "a22eeffff13a9c0322cd74529facb2f682790efcc71fe4d953d70fb305aedde4", - "bytes": 93739, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-01-full.png", - "engine": "firefox", - "sha256": "880883d92b031d0099579f3bbea04b5790ac87be285096494fd1d41be6ba54d5", - "bytes": 95755, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-02-45.png", - "engine": "firefox", - "sha256": "44176c2be3731babc8ec2b5221e401d065b917a93b47c69dc09ae83443ccb2e2", - "bytes": 99191, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-crosshair-03-small-again.png", - "engine": "firefox", - "sha256": "a22eeffff13a9c0322cd74529facb2f682790efcc71fe4d953d70fb305aedde4", - "bytes": 93739, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-00-before-tool-click.png", - "engine": "firefox", - "sha256": "c2721a6679a071285abc4c5b8cd5db66387fb6d5b4384eb62b383d78a7867779", - "bytes": 91180, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-01-after-click.png", - "engine": "firefox", - "sha256": "d016eca14a1a3310be7caa652e04092eb1d60fd7aa9fba244777d51491c085ce", - "bytes": 94061, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-draw-wires-02-after-drawing.png", - "engine": "firefox", - "sha256": "f702c3ebec9c53ea74bc96e17678fe444c882aed7a2222d3d7159452d6db26c9", - "bytes": 94720, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-load-rendered.png", - "engine": "firefox", - "sha256": "c159e2cd0020d29511fa98a424c8a584af8abdd44018e58d7fc73bfaa471f95c", - "bytes": 101522, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-loaded-css.png", - "engine": "firefox", - "sha256": "c2721a6679a071285abc4c5b8cd5db66387fb6d5b4384eb62b383d78a7867779", - "bytes": 91180, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-loaded.png", - "engine": "firefox", - "sha256": "c2721a6679a071285abc4c5b8cd5db66387fb6d5b4384eb62b383d78a7867779", - "bytes": 91180, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-sim-frame.png", - "engine": "firefox", - "sha256": "8fe0b33efdaf9c3a0a6843c54a4f1508eb68d736a67885d40ffc6a8b5c4b1506", - "bytes": 102937, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-sim-plot.png", - "engine": "firefox", - "sha256": "71e9863e4747d14f491fba39269066ca738c678a1ea9bc801064001e7cb85445", - "bytes": 144283, - "width": 1280, - "height": 720 - }, - { - "name": "eeschema-url-regex.png", - "engine": "firefox", - "sha256": "b4ba3159a23a61b31adf087c5c275d519c0fdce891cf41fd385374f3e276cd12", - "bytes": 101034, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-eeschema.html.png", - "engine": "firefox", - "sha256": "c2721a6679a071285abc4c5b8cd5db66387fb6d5b4384eb62b383d78a7867779", - "bytes": 91180, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-footprint_editor.html.png", - "engine": "firefox", - "sha256": "b9ce8aa9d9abbd32728da190ed65a9718201c5021ef67a0f46db93788cafe2ac", - "bytes": 139071, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-pcbnew.html.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "frame-runtime-symbol_editor.html.png", - "engine": "firefox", - "sha256": "65e96df0706d45e58752ecd1eade9fe7295d8a19cb4bb7cba0bf73b56621ec85", - "bytes": 71495, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-01-loaded.png", - "engine": "firefox", - "sha256": "70ea0dd9f90d2f77bb37fd8d94d4f8e7ed353e971a5a72a14ca410409dec68be", - "bytes": 65398, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-02-metrics.png", - "engine": "firefox", - "sha256": "70ea0dd9f90d2f77bb37fd8d94d4f8e7ed353e971a5a72a14ca410409dec68be", - "bytes": 65398, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-00-loaded.png", - "engine": "firefox", - "sha256": "81cfa4f0dc532d873beda50f9ae4cc8d27553975722e7313278d45b8c6d553d4", - "bytes": 120231, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-01-dialog.png", - "engine": "firefox", - "sha256": "bf113acba1fc65fe6b5e11fd01e9c62b66e386421cb82db3e8262e9fcb7dcdfd", - "bytes": 151967, - "width": 1280, - "height": 720 - }, - { - "name": "gerbview-print-02-reopened.png", - "engine": "firefox", - "sha256": "bf113acba1fc65fe6b5e11fd01e9c62b66e386421cb82db3e8262e9fcb7dcdfd", - "bytes": 151967, - "width": 1280, - "height": 720 - }, - { - "name": "import-settings-modal-stack.png", - "engine": "firefox", - "sha256": "9fe4d72ad53fd3b2262b4e0d1d2de6640b1c83c57e6509d393c4a13ca0aa19e0", - "bytes": 206463, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave-00-pcbnew-ready.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave-01-dialog-open.png", - "engine": "firefox", - "sha256": "dc0c7e34a3101cd0c54ab250a4f731a82b3b5463b8eda7f9d6c3e49e34f9bb0b", - "bytes": 171687, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-microwave.png", - "engine": "firefox", - "sha256": "aa69ba3349f5e6d6c608cd34a1ebace79eff118a2ff2a794b36e703a8d4395d0", - "bytes": 153450, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer-00-pcbnew-ready.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer-01-dialog-open.png", - "engine": "firefox", - "sha256": "66706697943908b2bc9f89fcdff79b3f60bb6c9ad918d0332e095da2f5fcfae5", - "bytes": 172011, - "width": 1280, - "height": 720 - }, - { - "name": "load-pcb-pic_programmer.png", - "engine": "firefox", - "sha256": "7770ead25e3136a0fe9ecc3515c7a57fcbfde42d2ee9bf66965d1a80d0e2fb5f", - "bytes": 304662, - "width": 1280, - "height": 720 - }, - { - "name": "occ-export-dialog.png", - "engine": "firefox", - "sha256": "14cd3d061a423f4e8c0a75793944fb5afdb005048a794b16936cc27397c9ec6e", - "bytes": 261455, - "width": 1280, - "height": 720 - }, - { - "name": "occ-export-done.png", - "engine": "firefox", - "sha256": "8a6bef1afcf21d2cfdcc694b0eecf68f831998caedd7542b64a7a147e0e36ad4", - "bytes": 250311, - "width": 1280, - "height": 720 - }, - { - "name": "occ-probe-done.png", - "engine": "firefox", - "sha256": "dc062e24106eed983c41d63db637b6bb5be886bb5013959c8ab675a71ff91eeb", - "bytes": 20684, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-context-menu.png", - "engine": "firefox", - "sha256": "03f101c60e72ba7f40fa4a519d2547a0d34cf8813985fc6a7ac6222b1f8da5a1", - "bytes": 163466, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-context-submenu.png", - "engine": "firefox", - "sha256": "86ba7dd4aa1040533eddb906fb689961f25c1ef63213fcd50bbcc227ce3f6891", - "bytes": 183352, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-dark-mode-loaded.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-00-before-tool-click.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-01-after-click.png", - "engine": "firefox", - "sha256": "426b98c0c05c80b504df5f441fc39fbe8a58da6b4a20a80ac546417391fe6be2", - "bytes": 154557, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-draw-lines-02-after-drawing.png", - "engine": "firefox", - "sha256": "1e56a4aaeeb6b0876f1cac6d5473cb87f9757b564c0541686abec2e7110343ad", - "bytes": 162571, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-loaded-css.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-loaded.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-move-00-before.png", - "engine": "firefox", - "sha256": "7236c3ff14751e8f5b5da5295c693341a9c53141651aad7c34eb62c056a81bdd", - "bytes": 154925, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-move-01-after.png", - "engine": "firefox", - "sha256": "e7f41335c260d1dd99b706f02c5d20f601241ca669cfbc568232415f0bc40415", - "bytes": 159481, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-sidebar-scrollbar-dragged.png", - "engine": "firefox", - "sha256": "4f2f476a2c6db6c9ef9919e36329ab3fa5d8a1eb50fabe647c36912919a19ce8", - "bytes": 149279, - "width": 1280, - "height": 720 - }, - { - "name": "pcbnew-sidebar-scrollbar.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-01-loaded.png", - "engine": "firefox", - "sha256": "38027e75473ad505f8675fe52f1c7d4e85a42e59b68ba6ea0d93964ed7db6d86", - "bytes": 61645, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-02-no-wizard.png", - "engine": "firefox", - "sha256": "38027e75473ad505f8675fe52f1c7d4e85a42e59b68ba6ea0d93964ed7db6d86", - "bytes": 61645, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-03-file-menu.png", - "engine": "firefox", - "sha256": "48c4d631fad6e3208fe84ba71dc333c86f4a3814e2fb7cbdadc874c6c15ec31f", - "bytes": 70091, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-04-save-as-dialog.png", - "engine": "firefox", - "sha256": "a38b717669ff3431d45685b2ed10bf1e1e7b82a32ce0fefa8a8759d4fbc543bf", - "bytes": 79049, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-04b-after-enter.png", - "engine": "firefox", - "sha256": "a38b717669ff3431d45685b2ed10bf1e1e7b82a32ce0fefa8a8759d4fbc543bf", - "bytes": 79049, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-context-menu.png", - "engine": "firefox", - "sha256": "27b7c26c669660b689153e5ca6af51f01b4a852b9a4ab72822dea359f85c89a3", - "bytes": 75014, - "width": 1280, - "height": 540 - }, - { - "name": "pl_editor-load-rendered.png", - "engine": "firefox", - "sha256": "a678f48f3db6917b00fd4527bc05de5c0b5d3ec2def9628b408e921abc0d1c93", - "bytes": 66269, - "width": 1280, - "height": 720 - }, - { - "name": "pl_editor-scrollbar-dragged.png", - "engine": "firefox", - "sha256": "d41a2e85f3266e2d8aee67424305e301bdd341d413c7d9c3e80c705adf0651bf", - "bytes": 65874, - "width": 1210, - "height": 460 - }, - { - "name": "pl_editor-scrollbar.png", - "engine": "firefox", - "sha256": "e8bd59f74769a46e41aa49c5a6e142907970cc839d1766550959fe8f3e222732", - "bytes": 69769, - "width": 1210, - "height": 523 - }, - { - "name": "probe-00-after-wizard.png", - "engine": "firefox", - "sha256": "23df9f84dff056f0778f6b5c0ee1a79f357afa7d4620a1f4f80348793e406bea", - "bytes": 152549, - "width": 1280, - "height": 720 - }, - { - "name": "probe-01-file-menu-open.png", - "engine": "firefox", - "sha256": "a3b5ab2be4356e78c2724441669e55f95019f64f712b61bf56e6aa4094b2815d", - "bytes": 165062, - "width": 1280, - "height": 720 - }, - { - "name": "probe-02-after-open-click.png", - "engine": "firefox", - "sha256": "b41631a83cdb97e05d8abb2a4ed3689a854b570e290f379b9afa51806ccfd7ef", - "bytes": 169337, - "width": 1280, - "height": 720 - }, - { - "name": "probe-03-late.png", - "engine": "firefox", - "sha256": "b41631a83cdb97e05d8abb2a4ed3689a854b570e290f379b9afa51806ccfd7ef", - "bytes": 169337, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-00-board-open.png", - "engine": "firefox", - "sha256": "81db2eb54e42addd144d68d6e4d1850a763a408a7cc7276317b4ccf405acc090", - "bytes": 141436, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-01-update-dialog.png", - "engine": "firefox", - "sha256": "2e72666dbc4c504cc439cb40a8fe33a6e708c2b9c7fa437cf8aea12d3c62876d", - "bytes": 184510, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-02-resync-first-R777.png", - "engine": "firefox", - "sha256": "f3ac8eaef6fc7b30c5f65332de85e9f731338a91bb4702a7a5a4c1c8466c95b3", - "bytes": 198146, - "width": 1280, - "height": 720 - }, - { - "name": "project-sync-03-resync-second-R888.png", - "engine": "firefox", - "sha256": "2f43a8dbdc5ac608396870281c0f2f8a5ab473fbf40f24a59b67cc962b4f72c9", - "bytes": 198596, - "width": 1280, - "height": 720 - }, - { - "name": "symbol_editor-01-loaded.png", - "engine": "firefox", - "sha256": "65e96df0706d45e58752ecd1eade9fe7295d8a19cb4bb7cba0bf73b56621ec85", - "bytes": 71495, - "width": 1280, - "height": 720 - }, - { - "name": "symbol_editor-02-metrics.png", - "engine": "firefox", - "sha256": "65e96df0706d45e58752ecd1eade9fe7295d8a19cb4bb7cba0bf73b56621ec85", - "bytes": 71495, - "width": 1280, - "height": 720 - }, - { - "name": "web-calculator.png", - "engine": "firefox", - "sha256": "b485f486376ad6981e637b5f7099325663511ce44b69adf9dec5e3a47705c776", - "bytes": 113676, - "width": 1280, - "height": 720 - }, - { - "name": "web-chrome-hidden.png", - "engine": "firefox", - "sha256": "e655f32bd91b8a4d776311cbebdccebdca476e1c7547e8bbe5bfc5521c9a042d", - "bytes": 107437, - "width": 1280, - "height": 720 - }, - { - "name": "web-chrome-restored.png", - "engine": "firefox", - "sha256": "5f9e52c5cde4dde10aea9f1b29254fbdadb8e784cff1addc852d047702e56867", - "bytes": 251747, - "width": 1280, - "height": 720 - }, - { - "name": "web-eeschema.png", - "engine": "firefox", - "sha256": "761a32eb24c87885bac9d3997aaa80a5c18dfe9e084b18d6fb669954eeec0bd5", - "bytes": 117815, - "width": 1280, - "height": 720 - }, - { - "name": "web-footprint_editor.png", - "engine": "firefox", - "sha256": "c00bf0e62bc72bab868c13e15b4ce976788f085d4c0598a7f52ea91c8c4f0939", - "bytes": 109074, - "width": 1280, - "height": 720 - }, - { - "name": "web-gerbview.png", - "engine": "firefox", - "sha256": "c015870448fde3362b32bf5c723cb90828d9383473fd11dcfe86b0c0a1a3506e", - "bytes": 72286, - "width": 1280, - "height": 720 - }, - { - "name": "web-pcbnew.png", - "engine": "firefox", - "sha256": "08b3cd699ffb1ee702286504e69dfb95077b53ef428114231e9f50022fb34072", - "bytes": 28621, - "width": 1280, - "height": 720 - }, - { - "name": "web-pl_editor.png", - "engine": "firefox", - "sha256": "b58841f992ac4d93dcaae1729bf25a22a8751d0e5e27d0aa8c4b3ef9f0b33aed", - "bytes": 27911, - "width": 1280, - "height": 720 - }, - { - "name": "web-read-only-viewer.png", - "engine": "firefox", - "sha256": "5831b7a8941754109e9d1c0f5216e9189cc6abe64b4718f338b41f56b9c84af2", - "bytes": 145519, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-missing-pcb-created.png", - "engine": "firefox", - "sha256": "c4ef619a19e7bcd46df6bd220521a839007c0a98880373ebf8ea3f3846a3330d", - "bytes": 150635, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-pcb-to-sch.png", - "engine": "firefox", - "sha256": "300e4a44f78ed6f02dc4716ecb4b85607c84f7d9f3f414ea0a0aa4b167e3cbe5", - "bytes": 119491, - "width": 1280, - "height": 720 - }, - { - "name": "web-switch-sch-to-pcb.png", - "engine": "firefox", - "sha256": "58b0df26f40ed68d1d9a1a48aa5a524eb6bd5233f21fc52d1d167618442fb458", - "bytes": 247284, - "width": 1280, - "height": 720 - }, - { - "name": "web-symbol_editor.png", - "engine": "firefox", - "sha256": "252bae8f80ff9042e0e971fab43e2722f7f7ff69598a534dc1266570e99157e2", - "bytes": 74236, - "width": 1280, - "height": 720 - }, - { - "name": "xface-preferences.png", - "engine": "firefox", - "sha256": "a1667f4697a7dd24e130492a82dccc5647c9c4c468cb21a66eda33f70327923c", - "bytes": 202590, - "width": 1280, - "height": 720 - } - ] -} diff --git a/tests/tools/screenshots/README.md b/tests/tools/screenshots/README.md index 593a255..31030f9 100644 --- a/tests/tools/screenshots/README.md +++ b/tests/tools/screenshots/README.md @@ -1,39 +1,45 @@ # Screenshot regression + Discord review tooling -One comparison engine + a churn-free updater + a Discord reporter for the e2e -screenshots. Design and rationale: `~/.claude/plans/…snowglobe.md` (or ask). +One comparison engine + a Discord reporter for the e2e screenshots; baseline +updates happen in the morelli review app +(https://pcbjam-morelli-staging.pcbjam-staging.workers.dev, repo +github.com/PCBJam/morelli). **Source of truth = CI's Linux render.** The dev never authors baselines on the -Mac (Mac fonts/GL ≠ CI). Instead, CI renders on every push; when a render change -is intentional you *promote* CI's artifact into the baselines. The environment -isn't pinned — if the host's Mesa/fonts drift, the gate lights up in Discord and -you just re-promote (broad + low-intensity change ⇒ likely drift). +Mac (Mac fonts/GL ≠ CI). CI renders on every push and uploads each run's +screenshots to R2 (`runs/pcbjam//`, 30-day retention, upload-run.ts); +when a render change is intentional you *promote* that run's screenshots in +morelli. The environment isn't pinned — if the host's Mesa/fonts drift, the +gate lights up in Discord and you just re-promote (broad + low-intensity +change ⇒ likely drift). -**Baselines live in R2, not git.** The PNGs sit in a private Cloudflare R2 -bucket (`pcbjam-ci-screenshots`), content-addressed as `sha256/.png` and -immutable; the committed `screenshot-manifest.json` pins each `/` -to a hash, so every git commit resolves its exact baselines. The local -`baseline-screenshots/` tree is a gitignored cache — `npm run screenshots:fetch` -materializes it; `promote` uploads new hashes and rewrites the manifest, and the -manifest diff is the only thing you commit. +**Baselines live entirely in R2, not git.** The PNGs sit in a private +Cloudflare R2 bucket (`pcbjam-ci-screenshots`), content-addressed as +`sha256/.png` and immutable; the R2-HOSTED manifest +`baselines/pcbjam/manifest.json` (written only by morelli + its seed script) +pins each `/` to a hash. The local `baseline-screenshots/` tree +and `.baseline-manifest.json` are gitignored caches — +`npm run screenshots:fetch-manifest && npm run screenshots:fetch` materializes +them. Nothing screenshot-related is committed. **Credentials** (S3 API, bucket-scoped, region `auto`): ``` CI_SCREENSHOTS_S3_ENDPOINT # https://.r2.cloudflarestorage.com CI_SCREENSHOTS_S3_BUCKET # optional, default pcbjam-ci-screenshots -CI_SCREENSHOTS_S3_ACCESS_KEY_ID # read-only pair in CI; read-write pair for promote +CI_SCREENSHOTS_S3_ACCESS_KEY_ID # read-only pair for fetch; CI's upload step maps in a write pair CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY ``` -CI holds the read-only pair as repo secrets; devs get the read-write pair from -the team vault (ask) and put it in `tests/.env` (gitignored, auto-loaded by -r2-store.ts; shell env vars take precedence): +CI holds the read-only pair as repo secrets (plus `CI_SCREENSHOTS_S3_WRITE_*` +for the run-upload step); devs get a read pair from the team vault (ask) and +put it in `tests/.env` (gitignored, auto-loaded by r2-store.ts; shell env vars +take precedence): ``` CI_SCREENSHOTS_S3_ENDPOINT=https://.r2.cloudflarestorage.com -CI_SCREENSHOTS_S3_ACCESS_KEY_ID= -CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY= +CI_SCREENSHOTS_S3_ACCESS_KEY_ID= +CI_SCREENSHOTS_S3_SECRET_ACCESS_KEY= ``` -Without credentials, fetch and the compare gate skip with a warning (secretless -CI callers stay green); promote refuses to run. +Without credentials, the manifest/baseline fetch and the compare gate skip with +a warning (secretless CI callers stay green). **Everything is per-engine.** Specs write `test-results//.png` (engine derived from the running browser via `stableShot`/`shotPath`); baselines @@ -45,27 +51,29 @@ The same spec on chromium + firefox is two independent gated screenshots. - `config.ts` — thresholds, baseline dirs, per-engine floors (calibrate!), clustering knobs. - `image-ops.ts` — PNG load/save, pixelmatch diff (AA-excluded), connected-component boxes, triptych compositing, size-cap resize. - `compare.ts` — the comparison engine: classify per-engine baselines vs `test-results//` → `test-results/screenshot-diff/report.json` + triptych/heatmap PNGs. `--pair` diffs two files. -- `promote.ts` — churn-free updater: pull a CI run's shots (`gh run download`) or `--from DIR`; overwrite a baseline only when pixels differ beyond the floor (verbatim bytes, no re-encode) → no git churn. - `perf-report.ts` — renders the track-only runtime-perf table (loadMs/openMs/FPS) with Δ vs the previous main run (fetched via `gh`). - `post-discord.ts` — the always-on CI-on-main report: SHA + e2e status + perf table, then screenshot triptychs (batched, size-capped, flood-collapsed). -- `changelog.ts` — Discord trigger B: git-history diff of the manifest between two revs, PNG bytes fetched from R2 (no build/GPU). - `noise.ts` — calibration: diff two identical-input renders → per-engine noise floor. -- `gen-manifest.ts` — regenerate `screenshot-manifest.json` ({name, engine, sha256, bytes, width, height}) from the local baseline cache; `--check` (gating in CI, credential-free) validates the schema and fails if baseline PNGs are ever re-committed to git. - `r2-store.ts` — minimal aws4fetch S3 client for the CAS bucket (get/put/exists by hash, downloads integrity-checked). -- `r2-sync.ts` — cache sync: `--pull` materializes `baseline-screenshots/` from the manifest (idempotent, deletes unlisted files), `--push` seeds/uploads, `--verify` HEADs every hash. +- `r2-sync.ts` — cache sync: `--manifest` downloads the R2-hosted baseline manifest, `--pull` materializes `baseline-screenshots/` from it (idempotent, deletes unlisted files), `--verify` HEADs every hash. +- `upload-run.ts` — CI-only: upload the run's renders + meta.json to `runs/pcbjam//` for morelli (needs the write pair; no-ops without credentials). - `spec-map.ts` — best-effort screenshot-name → spec-file attribution for captions (scans `stableShot`/`shotPath` literals). ## npm scripts (run from `tests/`) ``` -npm run screenshots:fetch # materialize the baseline cache from R2 (run before check; needs read creds) -npm run screenshots:check # gate: baselines vs test-results → report.json (exit 0; add --fail-on-change to gate) -npm run screenshots:promote -- --run # churn-free re-baseline from a CI run (or --from DIR; needs RW creds) -npm run screenshots:report -- --e2e pass # post the CI report to Discord (main+push only; needs DISCORD_WEBHOOK_URL) -npm run screenshots:changelog # post the baseline changelog (main+push only) -npm run screenshots:noise -- run1/ run2/ # calibrate floors -npm run screenshots:manifest # regenerate the manifest (--check to verify it's fresh) +npm run screenshots:fetch-manifest # download the R2-hosted baseline manifest (needs read creds) +npm run screenshots:fetch # materialize the baseline cache from R2 (run before check) +npm run screenshots:check # gate: baselines vs test-results → report.json (exit 0; add --fail-on-change to gate) +npm run screenshots:report -- --e2e pass # post the CI report to Discord (main+push only; needs DISCORD_WEBHOOK_URL) +npm run screenshots:noise -- run1/ run2/ # calibrate floors +npm run screenshots:upload-run -- --e2e pass # CI-only: upload the run's renders for morelli ``` +Baseline promotion (single or bulk) happens in morelli — pick the run, review +the diffs, Promote. It copies verbatim bytes into the CAS, updates the R2 +manifest atomically (with provenance: which run/branch/user), and snapshots the +previous manifest for revert. + ## Activation checklist - [x] `screenshot-manifest.json` generated ({name, engine} authoritative — derived from the per-engine baseline tree). - [x] `scale:'device'`→`'css'` normalized (no-op at CI's DSF=1). diff --git a/tests/tools/screenshots/changelog.ts b/tests/tools/screenshots/changelog.ts deleted file mode 100644 index ab846ed..0000000 --- a/tests/tools/screenshots/changelog.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Baseline changelog (Discord trigger B) — no build, no GPU. - * - * On push to main, diff the committed screenshot MANIFEST between two commits - * and post a "Baseline changelog" to Discord: a triptych (old | new+boxes | - * heatmap) for each CHANGED baseline, the image for each ADDED, and a titled - * line for each REMOVED. "added / removed" only mean anything as a git-history - * diff, which is why this is separate from the re-render drift gate. - * - * The PNGs themselves are not in git — each manifest entry pins a sha256 and - * the bytes are fetched from the R2 CAS bucket (objects are immutable, so the - * base rev's hashes are always still resolvable). Needs the read credential - * pair; a base rev whose manifest predates the R2 migration (v1) is skipped so - * the migration commit itself doesn't post hundreds of bogus entries. - * - * CLI (from tests/): - * tsx tools/screenshots/changelog.ts [--base REV] [--head REV] [--dry-run] [--force] - */ -import * as fs from 'fs'; -import * as path from 'path'; -import { execFileSync } from 'child_process'; -import { PNG } from 'pngjs'; -import { DIFF_OUT_DIR, MANIFEST_PATH, MANIFEST_VERSION, floorFor, labelText, splitKey, LABEL, type Manifest } from './config'; -import { comparePair, type Report } from './compare'; -import { savePng, withBottomLabel } from './image-ops'; -import { buildSpecResolver } from './spec-map'; -import { buildAttachments, paginate, postMessage } from './post-discord'; -import { missingEnv, storeFromEnv, type R2Store } from './r2-store'; - -function git(root: string, args: string[]): string { - return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim(); -} - -/** The screenshot manifest as committed at `rev`. Returns null ONLY when the - * file doesn't exist at that rev (pre-manifest era); any other git failure — - * shallow clone, bad revision, moved path — throws loudly instead of being - * mistaken for "predates the migration". Unparsable committed JSON also throws. */ -function manifestAt(repoRoot: string, rev: string, repoPath: string): Manifest | null { - let raw: string; - try { - raw = execFileSync('git', ['-C', repoRoot, 'show', `${rev}:${repoPath}`], { encoding: 'utf8' }); - } catch (e) { - const msg = ((e as { stderr?: Buffer }).stderr?.toString() ?? (e as Error).message).trim(); - if (/does not exist in|exists on disk, but not in/i.test(msg)) return null; - throw new Error(`git show ${rev}:${repoPath} failed: ${msg}`); - } - return JSON.parse(raw) as Manifest; -} - -function parseArgs(argv: string[]): Record { - const out: Record = {}; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === '--dry-run') out.dryRun = true; - else if (a === '--force') out.force = true; - else if (a === '--base') out.base = argv[++i]; - else if (a === '--head') out.head = argv[++i]; - } - return out; -} - -async function main(): Promise { - const args = parseArgs(process.argv.slice(2)); - const cwd = process.cwd(); - const repoRoot = git(cwd, ['rev-parse', '--show-toplevel']); - const head = (args.head as string) || 'HEAD'; - let base = (args.base as string) || ''; - if (!base) { - try { - base = git(cwd, ['rev-parse', `${head}^`]); - } catch { - console.log('[changelog] no parent commit — nothing to diff'); - return; - } - } - - // Repo-relative manifest path (e.g. tests/screenshot-manifest.json). - const manifestPath = path.relative(repoRoot, path.join(cwd, MANIFEST_PATH)).split(path.sep).join('/'); - const baseManifest = manifestAt(repoRoot, base, manifestPath); - const headManifest = manifestAt(repoRoot, head, manifestPath); - // A NEWER manifest than this tooling understands is a loud error, never a skip. - for (const [label, m] of [['head', headManifest], ['base', baseManifest]] as const) { - if (m && m.version > MANIFEST_VERSION) { - throw new Error(`${label} manifest is version ${m.version}, newer than this tooling (expects ${MANIFEST_VERSION}) — update the checkout`); - } - } - if (headManifest?.version !== MANIFEST_VERSION) { - console.log('[changelog] head manifest predates the R2 migration — nothing to diff'); - return; - } - if (baseManifest?.version !== MANIFEST_VERSION) { - console.log('[changelog] base manifest predates the R2 migration — skipping (migration commit)'); - return; - } - - // Engine-qualified key (`/`) → sha256, per rev. - const hashesOf = (m: Manifest): Map => - new Map(m.screenshots.map((e) => [`${e.engine}/${e.name}`, e.sha256])); - const baseHashes = hashesOf(baseManifest); - const headHashes = hashesOf(headManifest); - const added = [...headHashes.keys()].filter((k) => !baseHashes.has(k)); - const removed = [...baseHashes.keys()].filter((k) => !headHashes.has(k)); - const changed = [...headHashes.keys()].filter((k) => baseHashes.has(k) && baseHashes.get(k) !== headHashes.get(k)); - if (!added.length && !removed.length && !changed.length) { - console.log('[changelog] no baseline changes between', base.slice(0, 7), 'and', head); - return; - } - - // Tolerate missing credentials (warn + exit 0): this is a notification-only - // workflow — turning every manifest push red during a key rotation is worse - // than one missed changelog post. The warning names the fix. - const store = storeFromEnv(); - if (!store) { - console.warn(`[changelog] R2 credentials unset (${missingEnv().join(', ')}) — cannot fetch baseline bytes; skipping the changelog post`); - return; - } - - const outDir = path.join(cwd, DIFF_OUT_DIR); - fs.mkdirSync(outDir, { recursive: true }); - const report: Report = { - generatedFor: process.env.GITHUB_SHA || head, - changed: [], - added: [], - removed: [], - unchangedCount: 0, - driftLikely: false, - }; - const { specFor } = buildSpecResolver(cwd); - - // One unresolvable object must not abort the whole post — skip that image - // (with a loud note) and keep reporting the rest, like the old git-blob - // path null-skipped unresolvable blobs. - const skipped: string[] = []; - const hashPng = async (s: R2Store, hash: string, key: string): Promise => { - try { - return PNG.sync.read(await s.get(hash)); - } catch (e) { - skipped.push(`${key}: ${(e as Error).message}`); - return null; - } - }; - // Save a single captioned image (added/removed) and record it in the report. - const saveSingle = (img: PNG, key: string, status: 'added' | 'removed'): void => { - const rel = path.join(DIFF_OUT_DIR, `${key.replace('/', '_')}.${status}.png`); - savePng(path.join(cwd, rel), withBottomLabel(img, labelText(status, key, specFor(splitKey(key).name)), LABEL.colors[status])); - report[status].push({ name: key, image: rel }); - }; - - for (const key of added) { - const img = await hashPng(store, headHashes.get(key)!, key); - if (img) saveSingle(img, key, 'added'); - } - for (const key of removed) { - const img = await hashPng(store, baseHashes.get(key)!, key); - if (img) saveSingle(img, key, 'removed'); - } - for (const key of changed) { - const oldImg = await hashPng(store, baseHashes.get(key)!, key); - const newImg = await hashPng(store, headHashes.get(key)!, key); - if (!oldImg || !newImg) continue; - const { result, heatmap, triptych } = comparePair(oldImg, newImg, key, floorFor(key)); - const triptychRel = path.join(DIFF_OUT_DIR, `${key.replace('/', '_')}.triptych.png`); - const heatmapRel = path.join(DIFF_OUT_DIR, `${key.replace('/', '_')}.heatmap.png`); - savePng(path.join(cwd, triptychRel), withBottomLabel(triptych, labelText('changed', key, specFor(splitKey(key).name)), LABEL.colors.changed)); - savePng(path.join(cwd, heatmapRel), heatmap); - report.changed.push({ ...result, triptych: triptychRel, heatmap: heatmapRel }); - } - report.changed.sort((a, b) => b.changedRatio - a.changedRatio); - if (skipped.length) { - console.warn(`[changelog] skipped ${skipped.length} unresolvable image(s):\n ${skipped.join('\n ')}`); - } - - const sha7 = (process.env.GITHUB_SHA || head).slice(0, 7); - let subject = ''; - try { - subject = git(cwd, ['log', '-1', '--pretty=%s', head]); - } catch { /* ignore */ } - const header = - `🗂️ **Baseline changelog** · \`${sha7}\`` + - (subject ? `\n> ${subject}` : '') + - `\n${report.changed.length} changed, ${report.added.length} added, ${report.removed.length} removed` + - (skipped.length ? `\n⚠️ ${skipped.length} image(s) unresolvable in R2 — see the workflow log` : '') + - (report.removed.length ? '\n➖ REMOVED: ' + report.removed.map((r) => `\`${r.name}\``).join(', ') : ''); - - const { files, notes } = buildAttachments(cwd, report); - const messages = paginate(header + (notes.length ? '\n' + notes.join('\n') : ''), files); - - const isMainPush = process.env.GITHUB_REF === 'refs/heads/main' && process.env.GITHUB_EVENT_NAME === 'push'; - if (args.dryRun || (!args.force && !isMainPush)) { - for (const [i, m] of messages.entries()) { - console.log(`--- message ${i + 1}/${messages.length} (${m.files.length} files) ---`); - if (m.content) console.log(m.content); - for (const f of m.files) console.log(` [attach] ${f.name} (${f.buffer.length} bytes)`); - } - if (!args.dryRun) console.log('[changelog] not a push to main — not posting'); - return; - } - const webhook = process.env.DISCORD_WEBHOOK_URL; - if (!webhook) { - console.log('[changelog] DISCORD_WEBHOOK_URL unset — skipping'); - return; - } - for (const m of messages) await postMessage(webhook, m); - console.log(`[changelog] posted ${messages.length} message(s)`); -} - -if (require.main === module) { - main().catch((e) => { - console.error(`[changelog] ${e.message}`); - process.exitCode = 1; - }); -} diff --git a/tests/tools/screenshots/compare.ts b/tests/tools/screenshots/compare.ts index e85ae71..5b65baa 100644 --- a/tests/tools/screenshots/compare.ts +++ b/tests/tools/screenshots/compare.ts @@ -9,8 +9,8 @@ * The gate mode classifies every screenshot into changed / added / removed / * unchanged, writes per-change triptych + heatmap PNGs and a machine-readable * report.json into DIFF_OUT_DIR, and (unless --fail-on-change) exits 0 so it can - * run report-only first. post-discord.ts and the changelog workflow import the - * exported helpers rather than re-deriving the diff. + * run report-only first. post-discord.ts imports the exported helpers rather + * than re-deriving the diff. */ import * as fs from 'fs'; import * as path from 'path'; @@ -236,7 +236,14 @@ function main(): void { // dev who hasn't fetched. Either way skip without writing report.json — // post-discord posts a distinct "gate SKIPPED" line when it's absent. const gateManifest = loadManifest(root); - const wanted = (gateManifest?.screenshots ?? []).filter((e) => !isIgnored(`${e.engine}/${e.name}`)); + // No fetched manifest at all (creds absent, R2 down, or morelli never + // seeded) ⇒ the gate has no source of truth — skip rather than diff + // against whatever stale cache a previous run left behind. + if (!gateManifest) { + console.log(`[compare] no fetched ${MANIFEST_PATH} — run \`npm run screenshots:fetch-manifest\` (needs R2 credentials); skipping`); + return; + } + const wanted = gateManifest.screenshots.filter((e) => !isIgnored(`${e.engine}/${e.name}`)); const missing = wanted.filter((e) => !fs.existsSync(path.join(root, BASELINE_ROOT, e.engine, e.name))); if (wanted.length && missing.length) { const what = missing.length === wanted.length ? 'empty' : `INCOMPLETE (${missing.length}/${wanted.length} missing — partial fetch?)`; diff --git a/tests/tools/screenshots/config.ts b/tests/tools/screenshots/config.ts index cec778a..bffd632 100644 --- a/tests/tools/screenshots/config.ts +++ b/tests/tools/screenshots/config.ts @@ -20,16 +20,18 @@ import * as nodePath from 'path'; * other's PNG, and the surviving file was whichever parallel worker wrote last. * * The baseline PNGs are NOT committed: they live in a private R2 bucket, - * content-addressed by sha256, and the committed manifest - * (screenshot-manifest.json) is what pins each `/` to a hash. - * `npm run screenshots:fetch` (tools/screenshots/r2-sync.ts) materializes this - * tree from the manifest; promote.ts uploads new hashes and rewrites the - * manifest — the manifest diff is the only thing that lands in git. + * content-addressed by sha256, pinned by the R2-HOSTED manifest + * (baselines/pcbjam/manifest.json — nothing manifest-related is in git). + * `npm run screenshots:fetch-manifest && npm run screenshots:fetch` + * (tools/screenshots/r2-sync.ts) materializes this tree. Baselines are + * promoted from a CI run in the morelli review app + * (https://pcbjam-morelli-staging.pcbjam-staging.workers.dev) — the old + * promote.ts/git-manifest flow is retired. */ export const BASELINE_ROOT = 'baseline-screenshots'; -/** Manifest format version — bumped when baselines moved from git to R2. */ -export const MANIFEST_VERSION = 2; +/** Manifest format version — v3 = the R2-hosted manifest written by morelli (v2 was the committed-in-git era). */ +export const MANIFEST_VERSION = 3; /** Default private R2 bucket holding the content-addressed baselines. */ export const R2_DEFAULT_BUCKET = 'pcbjam-ci-screenshots'; @@ -37,6 +39,9 @@ export const R2_DEFAULT_BUCKET = 'pcbjam-ci-screenshots'; /** Key prefix for content-addressed objects: `sha256/<64-hex>.png`. */ export const R2_KEY_PREFIX = 'sha256/'; +/** The R2-hosted baseline manifest — THE source of truth. Written only by morelli (promote) and its seed script. */ +export const R2_BASELINES_MANIFEST_KEY = 'baselines/pcbjam/manifest.json'; + /** * Env vars for the bucket's S3 API (read-only keypair in CI, read-write for * devs running promote). Endpoint form: https://.r2.cloudflarestorage.com @@ -80,8 +85,14 @@ export const RESULTS_DIR = 'test-results'; /** Where compare.ts writes diff/heatmap/triptych artifacts (gitignored). */ export const DIFF_OUT_DIR = 'test-results/screenshot-diff'; -/** The manifest that records every expected screenshot + which engine renders it. */ -export const MANIFEST_PATH = 'screenshot-manifest.json'; +/** + * Local, GITIGNORED copy of the R2-hosted baseline manifest, downloaded by + * `npm run screenshots:fetch-manifest` (r2-sync --manifest). Everything + * downstream (fetch, compare, verify) reads this file; its absence means "no + * manifest fetched" and the screenshot gate skips. Deliberately NOT inside + * test-results/ — Playwright wipes that dir at the start of every invocation. + */ +export const MANIFEST_PATH = '.baseline-manifest.json'; /** * pixelmatch per-pixel settings. @@ -185,10 +196,22 @@ export function labelText(status: LabelStatus, key: string, spec: string | null) * `height` are sanity metadata (cheap pre-hash check on fetch, dimension info * in reviews). */ -export type ManifestEntry = { name: string; engine: string; sha256: string; bytes: number; width: number; height: number }; +export type ManifestEntry = { + name: string; + engine: string; + sha256: string; + bytes: number; + width: number; + height: number; + /** morelli-side provenance (which run/branch/user promoted this) — opaque to the CI tooling. */ + source?: unknown; +}; export type Manifest = { version: number; + pipeline?: string; storage: { bucket: string; keyPrefix: string }; + updatedAt?: string; + updatedBy?: string; screenshots: ManifestEntry[]; }; diff --git a/tests/tools/screenshots/gen-manifest.ts b/tests/tools/screenshots/gen-manifest.ts deleted file mode 100644 index a26cec2..0000000 --- a/tests/tools/screenshots/gen-manifest.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Generate tests/screenshot-manifest.json — the canonical list of expected - * screenshots AND the pointer to their bytes. Each entry is - * {name, engine, sha256, bytes, width, height}: the engine comes from the - * baseline cache tree (baseline-screenshots//.png), the sha256 - * resolves the PNG in the private R2 bucket (`sha256/.png`). The manifest - * is the SOURCE OF TRUTH — the local tree is a cache materialized from it by - * r2-sync (`npm run screenshots:fetch`). - * - * promote.ts is the single writer: it regenerates this file after applying and - * uploading a CI run's render. Regeneration therefore needs the local tree - * present; --check deliberately does NOT (it must stay credential- and - * network-free so it can gate every CI caller, secretless ones included). - * - * CLI (from tests/): tsx tools/screenshots/gen-manifest.ts [--check [--remote]] - * --check exit 1 unless the committed manifest is well-formed (v2 schema, - * sorted+unique entries, valid hashes) and no baseline PNG has been - * re-committed to git (resurrection guard). - * --remote with --check: also HEAD every hash in R2 (needs credentials; - * manual use only, not wired into CI). - */ -import * as fs from 'fs'; -import * as path from 'path'; -import { execFileSync } from 'child_process'; -import { PNG } from 'pngjs'; -import { - BASELINE_ROOT, - ENGINES, - IGNORE_SCREENSHOTS, - MANIFEST_PATH, - MANIFEST_VERSION, - R2_DEFAULT_BUCKET, - R2_KEY_PREFIX, - isIgnored, - listEngineKeys, - splitKey, - type Manifest, - type ManifestEntry, -} from './config'; -import { hashFile, missingEnv, storeFromEnv } from './r2-store'; - -/** Locale-independent (code-unit) ordering — the writer runs on dev machines, - * the --check gate on CI, and localeCompare's result depends on the host locale. */ -function compareEntries(a: { engine: string; name: string }, b: { engine: string; name: string }): number { - if (a.engine !== b.engine) return a.engine < b.engine ? -1 : 1; - return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; -} - -const NOTE = - 'Baselines live in R2, content-addressed by sha256 — the local baseline-screenshots/ tree is a cache ' + - '(`npm run screenshots:fetch`). Regenerated by `npm run screenshots:promote`; never edit hashes by hand. ' + - 'The name list is authoritative for removed-screenshot detection.'; - -/** Render the manifest JSON for the current baseline cache tree (tree must be present). */ -export function manifestJson(root: string): string { - const base = path.join(root, BASELINE_ROOT); - const screenshots: ManifestEntry[] = listEngineKeys(base) - .filter((key) => !isIgnored(key)) - .map((key) => { - const { engine, name } = splitKey(key); - const file = path.join(base, key); - const png = PNG.sync.read(fs.readFileSync(file)); - return { - name, - engine, - sha256: hashFile(file), - bytes: fs.statSync(file).size, - width: png.width, - height: png.height, - }; - }) - .sort(compareEntries); - - const manifest: Manifest & { _note: string } = { - _note: NOTE, - version: MANIFEST_VERSION, - storage: { bucket: R2_DEFAULT_BUCKET, keyPrefix: R2_KEY_PREFIX }, - screenshots, - }; - return JSON.stringify(manifest, null, 2) + '\n'; -} - -/** Regenerate the manifest on disk (used by the CLI and by promote.ts after apply). */ -export function writeManifest(root: string): void { - fs.writeFileSync(path.join(root, MANIFEST_PATH), manifestJson(root)); -} - -/** Schema + hygiene problems with the committed manifest (empty = healthy). Credential-free. */ -export function checkManifest(root: string): string[] { - const problems: string[] = []; - const p = path.join(root, MANIFEST_PATH); - if (!fs.existsSync(p)) return [`${MANIFEST_PATH} is missing`]; - let manifest: Manifest; - try { - manifest = JSON.parse(fs.readFileSync(p, 'utf8')) as Manifest; - } catch (e) { - return [`${MANIFEST_PATH} is not valid JSON: ${(e as Error).message}`]; - } - if (manifest.version !== MANIFEST_VERSION) problems.push(`version is ${manifest.version}, expected ${MANIFEST_VERSION}`); - if (!manifest.storage?.bucket || !manifest.storage?.keyPrefix) problems.push('storage.bucket/keyPrefix missing'); - if (!Array.isArray(manifest.screenshots) || manifest.screenshots.length === 0) { - problems.push('screenshots list is missing or empty'); - return problems; - } - - const engines = new Set(ENGINES); - let prev: ManifestEntry | null = null; - const seen = new Set(); - for (const e of manifest.screenshots) { - const id = `${e.engine}/${e.name}`; - if (!e.name?.toLowerCase().endsWith('.png')) problems.push(`${id}: name is not a .png`); - if (!engines.has(e.engine)) problems.push(`${id}: unknown engine`); - if (!/^[0-9a-f]{64}$/.test(e.sha256 ?? '')) problems.push(`${id}: sha256 is not 64 lowercase hex chars`); - if (!(e.bytes > 0) || !(e.width > 0) || !(e.height > 0)) problems.push(`${id}: bytes/width/height must be positive`); - if (IGNORE_SCREENSHOTS.has(e.name)) problems.push(`${id}: is in IGNORE_SCREENSHOTS and must not be listed`); - if (seen.has(id)) problems.push(`${id}: duplicate entry`); - seen.add(id); - if (prev && compareEntries(prev, e) > 0) { - problems.push(`${id}: not sorted (engine, then name)`); - } - prev = e; - } - - // Resurrection guard: baselines must never come back as committed files. - try { - const tracked = execFileSync('git', ['ls-files', '--', BASELINE_ROOT], { cwd: root, encoding: 'utf8' }).trim(); - if (tracked) { - problems.push( - `git-tracked PNGs under ${BASELINE_ROOT}/ — baselines live in R2, do not commit them:\n ` + - tracked.split('\n').slice(0, 5).join('\n ') - ); - } - } catch { - console.warn('[manifest] git unavailable — skipping the resurrection guard'); - } - return problems; -} - -async function main(): Promise { - const check = process.argv.includes('--check'); - const remote = process.argv.includes('--remote'); - const root = process.cwd(); - const outPath = path.join(root, MANIFEST_PATH); - - if (check) { - const problems = checkManifest(root); - if (problems.length) { - console.error(`[manifest] INVALID:\n - ${problems.join('\n - ')}`); - process.exitCode = 1; - return; - } - const manifest = JSON.parse(fs.readFileSync(outPath, 'utf8')) as Manifest; - console.log(`[manifest] ok — ${manifest.screenshots.length} screenshots across engines`); - if (remote) { - const store = storeFromEnv(); - if (!store) { - console.error(`[manifest] --remote needs credentials: set ${missingEnv().join(', ')}`); - process.exitCode = 2; - return; - } - const { verifyBaselines } = await import('./r2-sync'); - const missing = await verifyBaselines(root, store); - if (missing.length) { - console.error(`[manifest] ${missing.length} hash(es) missing in R2:\n ${missing.join('\n ')}`); - process.exitCode = 1; - } else { - console.log('[manifest] every hash present in R2'); - } - } - return; - } - - // Regeneration path: requires the cache tree (an absent tree would silently - // produce an empty manifest and disable removed-screenshot detection). - if (listEngineKeys(path.join(root, BASELINE_ROOT)).length === 0) { - console.error('[manifest] baseline cache is empty — run `npm run screenshots:fetch` first (or promote a run)'); - process.exitCode = 1; - return; - } - const json = manifestJson(root); - fs.writeFileSync(outPath, json); - const count = (JSON.parse(json) as Manifest).screenshots.length; - console.log(`[manifest] wrote ${MANIFEST_PATH} (${count} screenshots)`); -} - -if (require.main === module) { - main().catch((e) => { - console.error(`[manifest] ${(e as Error).message}`); - process.exitCode = 1; - }); -} diff --git a/tests/tools/screenshots/post-discord.ts b/tests/tools/screenshots/post-discord.ts index 611f220..ff86cc2 100644 --- a/tests/tools/screenshots/post-discord.ts +++ b/tests/tools/screenshots/post-discord.ts @@ -142,7 +142,7 @@ function buildHeader(report: Report | null, perfBlock: string, meta: { sha?: str } else if (!report.changed.length && !report.added.length && !report.removed.length) { lines.push('✅ no screenshot drift'); } else if (report.driftLikely) { - lines.push(`⚠️ **${report.changed.length} screenshots changed broadly** — looks like host env drift → re-promote (\`npm run screenshots:promote\`)`); + lines.push(`⚠️ **${report.changed.length} screenshots changed broadly** — looks like host env drift → re-promote in morelli (https://pcbjam-morelli-staging.pcbjam-staging.workers.dev)`); } else { lines.push( `⚠️ **screenshot drift**: ${report.changed.length} changed${byEngine(report.changed)}, ` + diff --git a/tests/tools/screenshots/promote.ts b/tests/tools/screenshots/promote.ts deleted file mode 100644 index f6d0a52..0000000 --- a/tests/tools/screenshots/promote.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Churn-free baseline updater — "promote CI's render". - * - * CI's x86 render is the source of truth. This pulls a CI run's screenshots - * (`gh run download`) — or a local dir via --from — and, for each one, overwrites - * the cached baseline ONLY when the decoded pixels differ beyond the per-engine - * floor. Unchanged baselines keep their bytes (never re-encoded), so their hash — - * and therefore the manifest — sees no churn. New shots are added; baselines with - * no render are reported as removal candidates and only dropped with --prune. - * - * Baselines live in a private R2 bucket (content-addressed; see r2-store.ts). - * The flow is: sync the local cache from the committed manifest → build/apply - * the plan locally → upload the updated/added bytes to R2 → regenerate the - * manifest. Only the manifest diff is committed; R2 objects are immutable and - * never deleted (--prune removes the manifest entry, old commits still resolve). - * Needs the READ-WRITE credential pair (see tools/screenshots/README.md). - * - * CLI (from tests/): - * tsx tools/screenshots/promote.ts --run [--repo owner/repo] [--prune] [--dry-run] - * tsx tools/screenshots/promote.ts --from [--prune] [--dry-run] - */ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { execFileSync } from 'child_process'; -import { BASELINE_ROOT, MANIFEST_PATH, floorFor, isIgnored, listEngineKeys, splitKey, type Manifest } from './config'; -import { writeManifest } from './gen-manifest'; -import { diffImages, loadPng } from './image-ops'; -import { hashBytes, missingEnv, storeFromEnv, type R2Store } from './r2-store'; -import { loadManifestV2, pool, pullBaselines } from './r2-sync'; - -/** key (`/`) → absolute cached baseline path. */ -function baselineIndex(root: string): Map { - const abs = path.join(root, BASELINE_ROOT); - const index = new Map(); - for (const key of listEngineKeys(abs)) index.set(key, path.join(abs, key)); - return index; -} - -function loadManifest(root: string): Manifest | undefined { - const p = path.join(root, MANIFEST_PATH); - if (!fs.existsSync(p)) return undefined; - try { - return JSON.parse(fs.readFileSync(p, 'utf8')) as Manifest; - } catch { - return undefined; - } -} - -/** Download a CI run's artifact and return the top-level test-results dir holding the shots. */ -function downloadRun(runId: string, repo?: string): string { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'promote-')); - const repoArgs = repo ? ['--repo', repo] : []; - execFileSync('gh', ['run', 'download', runId, '-D', tmp, ...repoArgs], { stdio: 'inherit' }); - // The artifact stores test-results/** — find that dir; its immediate *.png are the shots - // (exclude the nested screenshot-diff/ triptychs). - const stack = [tmp]; - while (stack.length) { - const dir = stack.pop()!; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - if (path.basename(dir) === 'test-results') return dir; - for (const e of entries) if (e.isDirectory()) stack.push(path.join(dir, e.name)); - } - throw new Error(`no test-results/ dir found in the downloaded artifact under ${tmp}`); -} - -type Plan = { updated: string[]; added: string[]; unchanged: string[]; removedCandidates: string[] }; - -function buildPlan(root: string, renderDir: string, manifest?: Manifest): { plan: Plan; apply: () => void } { - const baselines = baselineIndex(root); - const rendered = new Set(listEngineKeys(renderDir)); - // Never promote excluded screenshots (e.g. the flaky retinascale fullPage shot). - for (const key of [...rendered]) if (isIgnored(key)) rendered.delete(key); - for (const key of [...baselines.keys()]) if (isIgnored(key)) baselines.delete(key); - const plan: Plan = { updated: [], added: [], unchanged: [], removedCandidates: [] }; - const actions: Array<() => void> = []; - - for (const key of rendered) { - const src = path.join(renderDir, key); - const existing = baselines.get(key); - if (!existing) { - const dest = path.join(root, BASELINE_ROOT, key); - plan.added.push(key); - actions.push(() => { - fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.copyFileSync(src, dest); // verbatim bytes - }); - continue; - } - const d = diffImages(loadPng(existing), loadPng(src)); - const floor = floorFor(key); - if (!d.dimsMatch || d.changedRatio > floor.changedRatio) { - plan.updated.push(key); - actions.push(() => fs.copyFileSync(src, existing)); // verbatim bytes, no re-encode → no churn - } else { - plan.unchanged.push(key); // leave the cached file untouched - } - } - - // Removal candidates: a manifest-listed baseline this render didn't produce. - for (const [key, abs] of baselines) { - if (rendered.has(key)) continue; - const { engine, name } = splitKey(key); - if (manifest && !manifest.screenshots.some((e) => e.name === name && e.engine === engine)) continue; - plan.removedCandidates.push(key); - actions.push(() => {}); // pruning is opt-in (see main) - void abs; - } - - return { plan, apply: () => actions.forEach((a) => a()) }; -} - -/** Upload the applied updated/added baselines to R2 (skip-if-exists per hash). */ -async function uploadApplied(root: string, store: R2Store, keys: string[]): Promise { - let uploaded = 0; - await pool(keys, 8, async (key) => { - const bytes = fs.readFileSync(path.join(root, BASELINE_ROOT, key)); - if ((await store.put(hashBytes(bytes), bytes)) === 'uploaded') uploaded++; - }); - return uploaded; -} - -function parseArgs(argv: string[]): Record { - const out: Record = {}; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === '--run') out.run = argv[++i]; - else if (a === '--from') out.from = argv[++i]; - else if (a === '--repo') out.repo = argv[++i]; - else if (a === '--prune') out.prune = true; - else if (a === '--dry-run') out.dryRun = true; - } - return out; -} - -async function main(): Promise { - const args = parseArgs(process.argv.slice(2)); - const root = process.cwd(); - if (!args.run && !args.from) { - console.error('usage: promote.ts --run [--repo owner/repo] | --from [--prune] [--dry-run]'); - process.exitCode = 2; - return; - } - // Fail fast BEFORE downloading anything: a promote that can't upload would - // otherwise leave the manifest referencing hashes that don't exist in R2. - const store = storeFromEnv(); - if (!store) { - console.error(`[promote] R2 read-write credentials required: set ${missingEnv().join(', ')}`); - process.exitCode = 2; - return; - } - // Refuse a pre-migration checkout rather than warn: buildPlan would mark - // most renders "unchanged" (so never uploaded) while writeManifest pins - // their hashes anyway — committing a manifest that references objects R2 - // doesn't have. Sync the checkout to a post-migration revision first. - if (!loadManifestV2(root)) { - console.error(`[promote] ${MANIFEST_PATH} is not the R2-backed v2 format — refusing to promote on a pre-migration checkout`); - process.exitCode = 2; - return; - } - // The local tree is a cache — sync it to the committed manifest so the plan - // diffs against exactly what the manifest pins (a stale/absent cache would - // otherwise misreport adds/updates). - const { downloaded, cached, deleted } = await pullBaselines(root, store); - console.log(`[promote] cache synced: downloaded=${downloaded} cached=${cached} deleted=${deleted}`); - - const renderDir = args.from ? (args.from as string) : downloadRun(args.run as string, args.repo as string); - const manifest = loadManifest(root); - const { plan, apply } = buildPlan(root, renderDir, manifest); - - console.log( - `[promote] updated=${plan.updated.length} added=${plan.added.length} ` + - `unchanged=${plan.unchanged.length} removal-candidates=${plan.removedCandidates.length}` - ); - for (const n of plan.updated) console.log(` UPDATE ${n}`); - for (const n of plan.added) console.log(` ADD ${n}`); - for (const n of plan.removedCandidates) console.log(` REMOVE? ${n}${args.prune ? ' (pruning)' : ' (use --prune to delete)'}`); - - if (args.dryRun) { - console.log('[promote] dry-run — no files written, nothing uploaded'); - return; - } - apply(); - if (args.prune) { - const baselines = baselineIndex(root); - // Prune drops the local file (and, below, the manifest entry). The R2 - // object is deliberately kept — old commits must still resolve it. - for (const n of plan.removedCandidates) fs.rmSync(baselines.get(n)!, { force: true }); - } - // Upload BEFORE regenerating the manifest: a failure here leaves at worst an - // orphaned CAS object, never a committed manifest pointing at a missing hash. - const uploaded = await uploadApplied(root, store, [...plan.updated, ...plan.added]); - console.log(`[promote] uploaded ${uploaded} object(s) to R2`); - // Keep the manifest in lockstep with the baseline tree — a stale manifest silently - // disables removed-screenshot detection for anything added after the last regen. - writeManifest(root); - console.log(`[promote] done — commit the ${MANIFEST_PATH} diff (the only git-visible output)`); -} - -if (require.main === module) { - main().catch((e) => { - console.error(`[promote] ${(e as Error).message}`); - process.exitCode = 1; - }); -} diff --git a/tests/tools/screenshots/r2-store.ts b/tests/tools/screenshots/r2-store.ts index 3c717b0..019c3e1 100644 --- a/tests/tools/screenshots/r2-store.ts +++ b/tests/tools/screenshots/r2-store.ts @@ -99,6 +99,21 @@ export class R2Store { return 'uploaded'; } + /** + * Download an ARBITRARY key (the R2-hosted baseline manifest) — unlike + * get(), not content-addressed, so no hash verification is possible; null + * on 404 so the caller can degrade like the no-credentials path. + */ + async getKey(key: string): Promise { + const res = await this.fetchWithRetry(`${this.base}/${key}`, { method: 'GET' }); + if (res.status === 404) { + await res.arrayBuffer().catch(() => undefined); + return null; + } + if (res.status !== 200) throw new Error(`GET ${key} → HTTP ${res.status}`); + return Buffer.from(await res.arrayBuffer()); + } + /** * Upload to an ARBITRARY key (the per-run uploads under runs/…, consumed by * the morelli review app) — unlike put(), not content-addressed and always diff --git a/tests/tools/screenshots/r2-sync.ts b/tests/tools/screenshots/r2-sync.ts index d1a63ed..0e017f1 100644 --- a/tests/tools/screenshots/r2-sync.ts +++ b/tests/tools/screenshots/r2-sync.ts @@ -1,31 +1,38 @@ /** * Sync the local baseline-screenshots/ cache with the R2 CAS bucket. * - * The committed manifest (screenshot-manifest.json) is the source of truth; - * baseline-screenshots/ is a gitignored local cache materialized from it. + * The R2-HOSTED manifest (baselines/pcbjam/manifest.json, written only by the + * morelli review app + its seed script) is the source of truth. --manifest + * downloads it to the gitignored MANIFEST_PATH; everything downstream (--pull, + * --verify, compare.ts) reads that local copy, so a single fetch pins the + * whole run to one manifest version. * * CLI (from tests/): - * tsx tools/screenshots/r2-sync.ts --pull # manifest → local tree (CI fetch step + local gate) - * tsx tools/screenshots/r2-sync.ts --push # upload manifest entries missing in R2 (seeding) + * tsx tools/screenshots/r2-sync.ts --manifest # R2 manifest → local .baseline-manifest.json + * tsx tools/screenshots/r2-sync.ts --pull # fetched manifest → local tree (CI fetch step + local gate) * tsx tools/screenshots/r2-sync.ts --verify # HEAD every manifest hash, exit 1 on any miss * - * --pull without credentials (or with a pre-migration manifest) warns and - * exits 0, so secretless callers of the reusable CI workflow (release.yml, - * pcbjam deploy-staging.yml, fork PRs) stay green — compare.ts then skips its - * gate for the same reason. With credentials, any 404/corrupt object is - * collected and the run exits 1. + * --manifest and --pull without credentials warn and exit 0 — and --manifest + * DELETES a stale local manifest so the gate skips rather than comparing + * against outdated baselines. Secretless callers of the reusable CI workflow + * (release.yml, pcbjam deploy-staging.yml, fork PRs) therefore stay green; + * compare.ts skips its gate when no manifest was fetched. With credentials, + * any 404/corrupt object is collected and the run exits 1. + * + * (--push is gone with the git-manifest era: baseline bytes now enter the CAS + * only via morelli's promote, which copies them from the CI run uploads.) */ import * as fs from 'fs'; import * as path from 'path'; -import { BASELINE_ROOT, MANIFEST_PATH, MANIFEST_VERSION, listEngineKeys, type Manifest, type ManifestEntry } from './config'; +import { BASELINE_ROOT, MANIFEST_PATH, MANIFEST_VERSION, R2_BASELINES_MANIFEST_KEY, listEngineKeys, type Manifest, type ManifestEntry } from './config'; import { R2Store, hashFile, missingEnv, storeFromEnv } from './r2-store'; const CONCURRENCY = 16; -/** Parse the committed manifest if it is the R2-backed v2 format; null for a - * pre-migration (v1/absent/unparsable) manifest. A NEWER version throws — old - * tooling silently no-oping on a future format would disable the whole gate. */ -export function loadManifestV2(root: string): Manifest | null { +/** Parse the fetched manifest if it is the morelli-era v3 format; null when + * absent/unparsable (→ the gate skips). A NEWER version throws — old tooling + * silently no-oping on a future format would disable the whole gate. */ +export function loadManifest(root: string): Manifest | null { const p = path.join(root, MANIFEST_PATH); if (!fs.existsSync(p)) return null; let m: Manifest; @@ -40,6 +47,25 @@ export function loadManifestV2(root: string): Manifest | null { return m.version === MANIFEST_VERSION ? m : null; } +/** + * Download the R2-hosted baseline manifest to MANIFEST_PATH (atomic tmp+rename). + * Returns false when it could not be fetched — in which case any stale local + * copy is removed, so downstream steps skip instead of using old baselines. + */ +export async function fetchManifest(root: string, store: R2Store): Promise { + const dest = path.join(root, MANIFEST_PATH); + const bytes = await store.getKey(R2_BASELINES_MANIFEST_KEY); + if (!bytes) { + fs.rmSync(dest, { force: true }); + console.warn(`[r2-sync] ${R2_BASELINES_MANIFEST_KEY} not found in R2 — no baselines (seed via morelli first)`); + return false; + } + const tmp = `${dest}.tmp-${process.pid}`; + fs.writeFileSync(tmp, bytes); + fs.renameSync(tmp, dest); + return true; +} + /** Run `fn` over `items` with at most `limit` in flight. */ export async function pool(items: T[], limit: number, fn: (item: T) => Promise): Promise { let i = 0; @@ -60,8 +86,8 @@ export async function pullBaselines( root: string, store: R2Store ): Promise<{ downloaded: number; cached: number; deleted: number }> { - const manifest = loadManifestV2(root); - if (!manifest) throw new Error(`no v2 ${MANIFEST_PATH} — nothing to pull`); + const manifest = loadManifest(root); + if (!manifest) throw new Error(`no fetched ${MANIFEST_PATH} — run \`npm run screenshots:fetch-manifest\` first`); const base = path.join(root, BASELINE_ROOT); const wanted = new Map(); for (const e of manifest.screenshots) wanted.set(`${e.engine}/${e.name}`, e); @@ -99,35 +125,10 @@ export async function pullBaselines( return { downloaded, cached, deleted }; } -/** Upload every manifest entry's local file to R2 (skipping hashes already present). */ -export async function pushBaselines(root: string, store: R2Store): Promise<{ uploaded: number; existing: number }> { - const manifest = loadManifestV2(root); - if (!manifest) throw new Error(`no v2 ${MANIFEST_PATH} — run \`npm run screenshots:manifest\` first`); - const base = path.join(root, BASELINE_ROOT); - let uploaded = 0; - let existing = 0; - const errors: string[] = []; - await pool(manifest.screenshots, CONCURRENCY, async (e) => { - const file = path.join(base, e.engine, e.name); - try { - if (!fs.existsSync(file)) throw new Error('local file missing'); - const bytes = fs.readFileSync(file); - const hash = hashFile(file); - if (hash !== e.sha256) throw new Error(`local sha256 ${hash} ≠ manifest — regenerate the manifest`); - if ((await store.put(hash, bytes)) === 'uploaded') uploaded++; - else existing++; - } catch (err) { - errors.push(`${e.engine}/${e.name}: ${(err as Error).message}`); - } - }); - if (errors.length) throw new Error(`${errors.length} upload(s) failed:\n ${errors.join('\n ')}`); - return { uploaded, existing }; -} - /** HEAD every manifest hash; returns the missing keys. */ export async function verifyBaselines(root: string, store: R2Store): Promise { - const manifest = loadManifestV2(root); - if (!manifest) throw new Error(`no v2 ${MANIFEST_PATH}`); + const manifest = loadManifest(root); + if (!manifest) throw new Error(`no fetched ${MANIFEST_PATH} — run \`npm run screenshots:fetch-manifest\` first`); const missing: string[] = []; await pool(manifest.screenshots, CONCURRENCY, async (e) => { if (!(await store.exists(e.sha256))) missing.push(`${e.engine}/${e.name} (${e.sha256})`); @@ -136,20 +137,27 @@ export async function verifyBaselines(root: string, store: R2Store): Promise { - const mode = process.argv.find((a) => a === '--pull' || a === '--push' || a === '--verify'); + const mode = process.argv.find((a) => a === '--manifest' || a === '--pull' || a === '--verify'); if (!mode) { - console.error('usage: r2-sync.ts --pull | --push | --verify'); + console.error('usage: r2-sync.ts --manifest | --pull | --verify'); process.exitCode = 2; return; } const root = process.cwd(); - if (mode === '--pull' && !loadManifestV2(root)) { - console.log(`[r2-sync] ${MANIFEST_PATH} is not the R2-backed v2 format — nothing to fetch`); - return; + if (mode === '--pull' && !loadManifest(root)) { + console.log(`[r2-sync] no fetched ${MANIFEST_PATH} — skipping pull (run screenshots:fetch-manifest first)`); + return; // exit 0: manifest fetch skipped/failed → the gate skips, never gates on stale data } const store = storeFromEnv(); if (!store) { + if (mode === '--manifest') { + // Delete a stale copy so the downstream gate SKIPS instead of + // comparing against whatever manifest a previous run left behind. + fs.rmSync(path.join(root, MANIFEST_PATH), { force: true }); + console.log(`[r2-sync] R2 credentials unset (${missingEnv().join(', ')}) — skipping manifest fetch`); + return; // exit 0: secretless CI callers stay green + } if (mode === '--pull') { console.log(`[r2-sync] R2 credentials unset (${missingEnv().join(', ')}) — skipping baseline fetch`); return; // exit 0: secretless CI callers stay green @@ -159,12 +167,14 @@ async function main(): Promise { return; } - if (mode === '--pull') { + if (mode === '--manifest') { + if (await fetchManifest(root, store)) { + const manifest = loadManifest(root); + console.log(`[r2-sync] fetched ${MANIFEST_PATH}: ${manifest?.screenshots.length ?? 0} baselines (updated ${manifest?.updatedAt ?? '?'} by ${manifest?.updatedBy ?? '?'})`); + } + } else if (mode === '--pull') { const { downloaded, cached, deleted } = await pullBaselines(root, store); console.log(`[r2-sync] pull done: downloaded=${downloaded} cached=${cached} deleted=${deleted}`); - } else if (mode === '--push') { - const { uploaded, existing } = await pushBaselines(root, store); - console.log(`[r2-sync] push done: uploaded=${uploaded} already-present=${existing}`); } else { const missing = await verifyBaselines(root, store); if (missing.length) {