Skip to content

ref(snapshotting): Restructure CI export sidecar schema#259

Merged
cameroncooke merged 3 commits into
mainfrom
cameroncooke/ref/restructure-sidecar-schema
Apr 24, 2026
Merged

ref(snapshotting): Restructure CI export sidecar schema#259
cameroncooke merged 3 commits into
mainfrom
cameroncooke/ref/restructure-sidecar-schema

Conversation

@cameroncooke
Copy link
Copy Markdown
Contributor

@cameroncooke cameroncooke commented Apr 23, 2026

Restructures the JSON sidecar emitted via SNAPSHOTS_EXPORT_DIR from a flat blob into a nested schema with explicit top-level fields (display_name, group, diff_threshold) and a nested context object that groups runtime/environment data and per-preview author declarations.

Schema

{
  "display_name":    "...",   // canonical label for UI (author's .previewDisplayName, or derived fallback)
  "group":           "...",   // file path (#Preview) or container type display name (PreviewProvider)
  "diff_threshold":  0.0,     // optional, derived from preview precision
  "context": {
    "test_name":               "...",
    "accessibility_enabled":   false,
    "simulator": {             // optional — drops when no sim env vars
      "device_name":       "iPhone 17 Pro Max",
      "model_identifier":  "iPhone18,2"
    },
    "preview": {
      "index":                  0,
      "display_name":           "...",     // optional — raw .previewDisplayName(...) value, absent if author didn't set one
      "container_type_name":    "...",     // PreviewProvider type OR mangled PreviewRegistry conformance name
      "container_display_name": "...",     // derived pretty label from type/file name
      "preferred_color_scheme": "light",   // optional — from author's .preferredColorScheme(_:); enum: "light" | "dark"
      "orientation":            "portrait",// from author's .previewInterfaceOrientation(_:) or #Preview trait (default "portrait")
      "line":                   60         // optional — present only for #Preview macro
    }
  }
}

Key design decisions

  • Top-level fields surface anything a dashboard filters/groups by. Raw metadata stays under context.
  • context.preview groups everything the author declared about this specific preview. display_name, preferred_color_scheme, orientation, and line all flow from per-preview modifiers or the #Preview macro invocation site. Sits next to container identity (container_type_name, container_display_name) so each preview record is self-contained.
  • context.simulator is the runtime environment — sourced purely from SIMULATOR_DEVICE_NAME / SIMULATOR_MODEL_IDENTIFIER env vars. Orientation is explicitly not here — despite the simulator being rotated to match during capture, the canonical value is the author's declaration.
  • preferred_color_scheme mirrors SwiftUI's .preferredColorScheme(_:) modifier. Mapped at the source (SwiftUI.ColorScheme.stringValue now returns String?) so @unknown default drops out instead of emitting "unknown".
  • container_type_name is the type that declared the preview, not the view being previewed. For PreviewProvider this is your struct (HackerNews.CommentView_Preview); for #Preview it's a compiler-synthesized PreviewRegistry conformance (mangled name). A "view-under-test" field would require either fragile SwiftUI reflection or a new preference-key modifier — out of scope here.
  • container_display_name is a derived pretty label, not author-declared. Doc-commented at the source so future readers don't re-ask.
  • Dead fields removed from SnapshotContext: previewId (redundant — always "\(index)"), appStoreSnapshot (not emitted anywhere after the refactor), declaredDevice (filtered upstream — PreviewBaseTest already skips previews whose declared device doesn't match the host sim).

Sample sidecars

Four examples covering the full matrix: PreviewProvider vs #Preview macro × named vs anonymous × with/without preferred_color_scheme. Samples reflect the current schema; the on-disk fixtures under /private/tmp/snapshots25/iphone will regenerate to this shape on next export.

1. PreviewProvider with .previewDisplayName + .preferredColorScheme(.dark)

Shows preferred_color_scheme + orientation nested under preview, author-set display_name, readable container_type_name (no mangling), index disambiguating multiple previews in the same provider.

struct CommentView_Preview: PreviewProvider {
  static var previews: some View {
    Group {
      CommentView(...)
        .previewDisplayName("Light mode")           // index 0
      CommentView(...)
        .preferredColorScheme(.dark)
        .previewDisplayName("Dark mode")            // ← index 1 (this snapshot)
    }
  }
}
{
  "context" : {
    "accessibility_enabled" : false,
    "preview" : {
      "container_display_name" : "Comment View Preview",
      "container_type_name" : "HackerNews.CommentView_Preview",
      "display_name" : "Dark mode",
      "index" : 1,
      "orientation" : "portrait",
      "preferred_color_scheme" : "dark"
    },
    "simulator" : {
      "device_name" : "iPhone 17 Pro Max",
      "model_identifier" : "iPhone18,2"
    },
    "test_name" : "-[HackerNewsSnapshotTest portrait-Comment View Preview-1-6]"
  },
  "display_name" : "Dark mode",
  "group" : "Comment View Preview"
}

2. PreviewProvider + ForEach (multiple indexed previews, all named)

One container, six previews. Each gets a distinct index and a per-iteration .previewDisplayName("Indentation \(i)"). The container_type_name is shared; the individual display_name + index identify the specific snapshot.

struct CommentViewIndentation_Preview: PreviewProvider {
  static var previews: some View {
    ForEach(0..<6) { i in
      CommentRow(indentation: i)
        .previewDisplayName("Indentation \(i)")    // ← "Indentation 0" when i == 0
    }
  }
}
{
  "context" : {
    "accessibility_enabled" : false,
    "preview" : {
      "container_display_name" : "Comment View Indentation Preview",
      "container_type_name" : "HackerNews.CommentViewIndentation_Preview",
      "display_name" : "Indentation 0",
      "index" : 0,
      "orientation" : "portrait"
    },
    "simulator" : {
      "device_name" : "iPhone 17 Pro Max",
      "model_identifier" : "iPhone18,2"
    },
    "test_name" : "-[HackerNewsSnapshotTest portrait-Comment View Indentation Preview-0-8]"
  },
  "display_name" : "Indentation 0",
  "group" : "Comment View Indentation Preview"
}

3. #Preview macro, anonymous

No .previewDisplayNamepreview.display_name is absent. Top-level display_name falls back to "At line #60". container_type_name is the mangled PreviewRegistry conformance. container_display_name is derived from the filename (BookmarksScreen.swift"Bookmarks Screen"). group is the file path.

// BookmarksScreen.swift
#Preview {                                        // ← line 60
  BookmarksScreen()
}
{
  "context" : {
    "accessibility_enabled" : false,
    "preview" : {
      "container_display_name" : "Bookmarks Screen",
      "container_type_name" : "HackerNews.$s10HackerNews0026BookmarksScreenswift_DsAGgfMX59_0_33_02471A937C398E062128603F3D06A485Ll7PreviewfMf_15PreviewRegistryfMu_",
      "index" : 0,
      "line" : 60,
      "orientation" : "portrait"
    },
    "simulator" : {
      "device_name" : "iPhone 17 Pro Max",
      "model_identifier" : "iPhone18,2"
    },
    "test_name" : "-[HackerNewsSnapshotTest portrait-Bookmarks Screen-0-3]"
  },
  "display_name" : "At line #60",
  "group" : "HackerNews/BookmarksScreen.swift"
}

4. #Preview macro with .previewDisplayName

All macro-preview fields populated: preview.display_name set, line present, mangled container_type_name, container_display_name derived from filename.

// CommentRow.swift
#Preview("HTML case 1") {                         // ← line 152
  CommentRow(text: "<b>bold</b>")
}
{
  "context" : {
    "accessibility_enabled" : false,
    "preview" : {
      "container_display_name" : "Comment Row",
      "container_type_name" : "HackerNews.$s10HackerNews0021CommentRowswift_IfFDefMX151_0_33_B693E36E869B3E541838B120FF6BB995Ll7PreviewfMf_15PreviewRegistryfMu_",
      "display_name" : "HTML case 1",
      "index" : 0,
      "line" : 152,
      "orientation" : "portrait"
    },
    "simulator" : {
      "device_name" : "iPhone 17 Pro Max",
      "model_identifier" : "iPhone18,2"
    },
    "test_name" : "-[HackerNewsSnapshotTest portrait-Comment Row-0-14]"
  },
  "display_name" : "HTML case 1",
  "group" : "HackerNews/CommentRow.swift"
}

Closes EME-1073

Restructures the JSON sidecar emitted via SNAPSHOTS_EXPORT_DIR from a flat
blob into a nested schema with explicit top-level fields for dashboard
filtering and a nested `context` bag for forensic/diagnostic data.

Refs EME-1073
@cameroncooke cameroncooke marked this pull request as ready for review April 23, 2026 14:05
Renames and relocates the sidecar's color scheme field. The value reflects
the author-declared `.preferredColorScheme(_:)` modifier, not a rendered
output, so it belongs under `context` (forensic data) with a name that
mirrors the SwiftUI API.

Refs EME-1073
… preview

Removes the top-level `tags` object. Its `device` field was a duplicate of
`context.simulator.device_name`, and its `orientation` field is a per-preview
author declaration (from `.previewInterfaceOrientation(_:)` or `#Preview`
traits), not a simulator property. Both now live where their provenance
points:

- `context.preview.orientation` joins the other per-preview author
  declarations (`display_name`, `preferred_color_scheme`).
- `device_name` remains under `context.simulator` as the single source of
  truth.

Refs EME-1073
@cameroncooke cameroncooke merged commit 83131a5 into main Apr 24, 2026
7 checks passed
@cameroncooke cameroncooke deleted the cameroncooke/ref/restructure-sidecar-schema branch April 24, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants