P3.1: iOS goldens @2x + PNG optimizer + Makefile record/verify targets

- SnapshotGalleryTests rendered at displayScale: 2.0 (was native 3.0)
  → 49MB → 15MB (~69% reduction)
- Records via SNAPSHOT_TESTING_RECORD=1 env var (no code edits needed)
- scripts/optimize_goldens.sh runs zopflipng (or pngcrush fallback)
  over both iOS and Android golden dirs
- scripts/{record,verify}_snapshots.sh one-command wrappers
- Makefile targets: make {record,verify,optimize}-snapshots

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 23:45:02 -05:00
parent 6f2fb629c9
commit 3bac38449c
63 changed files with 318 additions and 7 deletions

View File

@@ -25,9 +25,26 @@
//
// Recording goldens
// -----------------
// Set `isRecording = true` in `setUp()`, run the target, then flip back to
// `false` before committing. CI fails the build if a screen diverges from
// its golden by more than the precision threshold.
// Preferred: `make record-snapshots` (or `./scripts/record_snapshots.sh
// --ios-only`). The script exports `SNAPSHOT_TESTING_RECORD=1` in the
// xcodebuild env, deletes the old `__Snapshots__/SnapshotGalleryTests`
// directory, runs the target, then invokes the shared PNG optimizer.
//
// Manual override: set the `SNAPSHOT_TESTING_RECORD` env var to `1` in
// the Xcode scheme's Test action (Edit Scheme Test Arguments
// Environment Variables) and re-run the test target. CI fails the
// build if a screen diverges from its golden by more than the
// precision threshold.
//
// Rendering scale
// ---------------
// We force `displayScale: 2.0` on every snapshot. The iPhone 15 /
// iPhone 13 simulators default to the device's native 3x, which on a
// full-screen gradient-heavy SwiftUI view produced 8001000 KB PNGs
// per image. @2x halves the linear dimensions (2.25x fewer pixels) and
// is still plenty to catch layout regressions. Combined with
// `scripts/optimize_goldens.sh` (zopflipng / pngcrush) this keeps us
// under the 150 KB per-image budget enforced by CI.
//
@preconcurrency import SnapshotTesting
@@ -39,8 +56,14 @@ import ComposeApp
@MainActor
final class SnapshotGalleryTests: XCTestCase {
// Flip to true locally, run the scheme, revert, commit.
private static let recordMode: SnapshotTestingConfiguration.Record = .missing
// Record mode is driven by the `SNAPSHOT_TESTING_RECORD` env var so
// `scripts/record_snapshots.sh` can flip it without mutating this file.
// When the var is unset/empty we only write missing goldens (`.missing`)
// so local dev runs never silently overwrite committed PNGs.
private static var recordMode: SnapshotTestingConfiguration.Record {
let env = ProcessInfo.processInfo.environment["SNAPSHOT_TESTING_RECORD"] ?? ""
return (env == "1" || env.lowercased() == "true") ? .all : .missing
}
override func invokeTest() {
withSnapshotTesting(record: Self.recordMode) {
@@ -63,6 +86,12 @@ final class SnapshotGalleryTests: XCTestCase {
private static let pixelPrecision: Float = 0.97
private static let perceptualPrecision: Float = 0.95
/// Force @2x rendering regardless of the simulator device's native
/// `displayScale`. See the class-level "Rendering scale" comment for
/// rationale. 2.0 keeps PNGs under the size budget without sacrificing
/// the structural detail our parity gallery cares about.
private static let forcedDisplayScale: CGFloat = 2.0
private func snap<V: View>(
_ name: String,
file: StaticString = #filePath,
@@ -80,7 +109,10 @@ final class SnapshotGalleryTests: XCTestCase {
precision: Self.pixelPrecision,
perceptualPrecision: Self.perceptualPrecision,
layout: .device(config: .iPhone13),
traits: .init(userInterfaceStyle: .light)
traits: .init(traitsFrom: [
UITraitCollection(userInterfaceStyle: .light),
UITraitCollection(displayScale: Self.forcedDisplayScale),
])
),
named: "\(name)_light",
file: file,
@@ -93,7 +125,10 @@ final class SnapshotGalleryTests: XCTestCase {
precision: Self.pixelPrecision,
perceptualPrecision: Self.perceptualPrecision,
layout: .device(config: .iPhone13),
traits: .init(userInterfaceStyle: .dark)
traits: .init(traitsFrom: [
UITraitCollection(userInterfaceStyle: .dark),
UITraitCollection(displayScale: Self.forcedDisplayScale),
])
),
named: "\(name)_dark",
file: file,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1024 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 616 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 KiB

After

Width:  |  Height:  |  Size: 231 KiB