Swift Package toolchain to build modern static reports for pointfreeco/swift-snapshot-testing.
Current version: 0.1.0
It provides:
- JSON report output.
- JUnit XML output with attachment entries.
- HTML output (stitch-inspired look) with pass/fail/skip status and attachment preview.
- HTML layout groups assertions by snapshot name (for both passed and failed tests).
- Aggregation of multiple test runs into one report.
- Custom HTML template support via Stencil.
- Reporter protocol architecture (
SnapshotReporter) with one implementation per format. - Swift 6 concurrency-ready APIs (
Sendable, actor-based runtime components). - Swift Testing-based package tests.
- XCTest + Swift Testing-compatible assertion surface.
- Device/runtime compatibility validation for snapshot presets.
- Auto-record behavior when reference assets do not exist (configurable).
- Advanced image diff attachments on failures (CoreImage).
- Per-run output directory support for test plan/package aggregation (
SNAPSHOT_REPORT_OUTPUT_DIR,--input-dir). - xcresult input — ingest
.xcresultbundles fromxcodebuild testdirectly (--xcresult). - Pass snapshot catalog from xcresult — when passed tests have no xcresult image attachments, local
__Snapshots__references are attached automatically for HTML catalog review. - odiff integration — SIMD-accelerated pixel diff images via the
odiffbinary, run automatically after merge (--odiff). - Design reference links — optional
referenceURLper test case, rendered as a button in the HTML report (Zeplin, Figma, or any URL). - Xcode project inspection —
inspectsubcommand to detect snapshot targets and generate CI configuration. - CLI progress UI — Noora-inspired processing steps and progress messages for load/extract/merge/write.
- Parallel processing — JSON ingestion, xcresult extraction, attachment copy, and report writers run in parallel (
--jobs). - APFS-safe attachment naming — oversized attachment filenames are shortened with a hash suffix and warning output.
All reporters conform to SnapshotReporter and are implemented in separate files:
Sources/SnapshotReportCore/Reporters/JSON/JSONSnapshotReporter.swiftSources/SnapshotReportCore/Reporters/JUnit/JUnitSnapshotReporter.swiftSources/SnapshotReportCore/Reporters/HTML/HTMLSnapshotReporter.swift
Dispatcher:
Sources/SnapshotReportCore/Reporting/ReportWriters.swift
Protocol:
Sources/SnapshotReportCore/Reporting/SnapshotReporter.swift
swift buildbrew tap oscarcv/tap
brew install snapshot-reportThis installs the snapshot-report CLI from prebuilt release artifacts (arm64 and x86_64).
GitHub Actions workflows included in this repository:
CI(.github/workflows/ci.yml): build, test, and CLI smoke test on push/PR.Dependency Review(.github/workflows/dependency-review.yml): blocks risky dependency changes in PRs.CodeQL(.github/workflows/codeql.yml): static security analysis for Swift on push/PR + weekly schedule.
Repository best-practice files included:
CONTRIBUTING.mdSECURITY.md.github/pull_request_template.md.github/ISSUE_TEMPLATE/*.github/dependabot.yml
Release automation included:
.github/workflows/release.yml- Builds
snapshot-reportfor macOSarm64andx86_64on tag push (v*) - Uploads artifacts to GitHub Releases
- Updates
oscarcv/tapformula automatically
- Builds
.github/workflows/nightly.yml- Runs on PR merges into
main - Publishes/updates a moving
nightlyprerelease (arm64+x86_64) - Updates
oscarcv/tapnightly formula (snapshot-report-nightly)
- Runs on PR merges into
Examples are isolated from the package source under:
examples/lib: standalone examples package with UIKit/SwiftUI modules and snapshot package test targets.examples/app: Xcode apps project with UIKit and SwiftUI app variants plus per-variant test plans.
Generate the app project:
cd examples/app
./Scripts/generate_project.shRun the full example pass flow (explicit xcresult paths + report generation):
./Scripts/run_pass_report.shswift run snapshot-report \
--input .artifacts/run-1.json \
--input .artifacts/run-2.json \
--format json,junit,html \
--output .artifacts/report \
--name "iOS Snapshot Regression"swift run snapshot-report \
--input-dir .artifacts/snapshot-runs \
--format json,junit,html \
--output .artifacts/reportNo custom assertion layer needed — point at the .xcresult produced by xcodebuild test. Use -resultBundlePath to avoid any DerivedData lookup:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-resultBundlePath .artifacts/xcresult/MyApp.xcresult
swift run snapshot-report \
--xcresult .artifacts/xcresult/MyApp.xcresult \
--format json,junit,html \
--output .artifacts/report- Failed tests: grouped by assertion/snapshot name with horizontal order
Snapshot - Diff - Failure. - Passed tests: grouped by assertion/snapshot name and rendered as horizontal variant rows.
- Image sizing: report CSS caps display width to
320pxand preserves aspect ratio.
Mix JSON runs and xcresult bundles freely:
swift run snapshot-report \
--xcresult .artifacts/xcresult/MyApp.xcresult \
--input .artifacts/extra-run.json \
--output .artifacts/reportUse --verbose to print detailed processing diagnostics (inputs, phase progress, odiff usage, and timings):
swift run snapshot-report \
--input-dir .artifacts/snapshot-runs \
--output .artifacts/report \
--format json,junit,html \
--verboseodiff is a SIMD-accelerated image diff tool that produces highlighted difference images. Install it first:
brew install dmtrKovalenko/tap/odiffodiff runs automatically if it is found on PATH. To specify the binary path explicitly:
swift run snapshot-report \
--input-dir .artifacts/snapshot-runs \
--odiff /usr/local/bin/odiff \
--output .artifacts/reportFor each failed test that has both a reference ("Snapshot") and an actual ("Actual Snapshot") attachment, an "odiff" diff image is appended and displayed in the HTML report.
swift run snapshot-report \
--input .artifacts/run.json \
--format html \
--output .artifacts/report \
--html-template ./my-report.stencilOutputs:
.artifacts/report/report.json.artifacts/report/report.junit.xml.artifacts/report/html/index.html.artifacts/report/html/attachments/*
The inspect subcommand scans a .xcodeproj to detect snapshot test targets and output recommended configuration.
swift run snapshot-report inspect --project MyApp.xcodeprojWith a GitLab CI scheduled-pipeline snippet:
swift run snapshot-report inspect --project MyApp.xcodeproj --gitlabExample output:
=== SnapshotReportKit Inspection: MyApp.xcodeproj ===
Snapshot testing targets detected:
• MyAppSnapshotTests
Recommended environment variables to set in each scheme's test action:
SNAPSHOT_REPORT_OUTPUT_DIR = $(SRCROOT)/.artifacts/snapshot-runs
SRCROOT = $(SRCROOT)
SCHEME_NAME = <your scheme name>
GIT_BRANCH = $(GIT_BRANCH)
TEST_PLAN_NAME = <your test plan name>
Add this call at the start of each snapshot test suite's setUp():
configureSnapshotReport(reportName: "<TargetName> Snapshots")
# === Suggested .gitlab-ci.yml snippet for scheduled snapshot runs ===
snapshot-tests:
stage: test
script:
- xcodebuild test -project MyApp.xcodeproj -scheme MyApp ...
artifacts:
paths:
- .artifacts/snapshot-runs/
- .artifacts/snapshot-report/
reports:
junit: .artifacts/snapshot-report/report.junit.xml
only:
- schedules
Each --input file is a SnapshotReport JSON document.
{
"name": "Snapshot Tests",
"generatedAt": "2026-02-20T10:00:00Z",
"metadata": {
"platform": "iOS",
"device": "iPhone 16"
},
"suites": [
{
"name": "CheckoutSnapshots",
"tests": [
{
"id": "F15A36E7-4ADE-4D46-8159-8DE96813F08A",
"name": "testCheckoutCard",
"className": "CheckoutSnapshotsTests",
"status": "failed",
"duration": 0.152,
"referenceURL": "https://app.zeplin.io/project/abc/screen/123",
"failure": {
"message": "Snapshot mismatch",
"file": "/path/to/CheckoutSnapshotsTests.swift",
"line": 44,
"diff": "Pixel mismatch around CTA area"
},
"attachments": [
{
"name": "Snapshot",
"type": "png",
"path": "/absolute/path/reference.png"
},
{
"name": "Actual Snapshot",
"type": "png",
"path": "/absolute/path/actual.png"
}
]
}
]
}
]
}Attachment type values:
png: previewed inline in HTML.dumportext: rendered as text block in HTML.binary: linked for download.
referenceURL is optional. When present it renders a "View Reference" link in the HTML report.
Only these package products are meant for external consumers:
SnapshotReportTesting(library): snapshot assertions + automatic run recording for test targets.snapshot-report(executable): merges run inputs and emits JSON/JUnit/HTML reports.
SnapshotReportTesting extends pointfreeco/swift-snapshot-testing with:
- Report-aware assertions
- Automatic JSON run report generation at test bundle end
- Multi-appearance snapshot assert (
light+darkby default) - Optional high contrast variants (
highContrastLight,highContrastDark) - Device preset validation against runtime iOS major version
- Advanced image diff attachment generation on image mismatches (CoreImage)
- Actual snapshot attachment for odiff post-processing
- Optional
referenceURLper assertion (Zeplin, Figma, or any design link)
- Add this package to your Xcode project.
- Link your test target with:
SnapshotTestingSnapshotReportTesting
- (Optional) add env vars in your test scheme:
SNAPSHOT_REPORT_OUTPUT_DIR=.artifacts/snapshot-runsSNAPSHOT_REPORT_NAME=My App Snapshot Tests
Swift Testing runs tests in parallel by default. This package is parallel-safe for recording because it uses actor-isolated collectors/runtime and supports per-run output directories (SNAPSHOT_REPORT_OUTPUT_DIR) for aggregation.
import Testing
import SnapshotTesting
import SnapshotReportTesting
@Suite("Login Snapshots")
struct LoginSnapshots {
private static let reportConfigured: Void = {
configureSnapshotReport(
reportName: "iOS Snapshot Tests",
metadata: ["platform": "iOS", "suite": "Login"]
)
configureSnapshotAssertionDefaults(
.init(
device: .iPhone13,
configuredOSMajorVersion: 26,
captureHeight: .large,
highContrastReport: false
)
)
}()
@Test("login screen light/dark")
func loginDefaultModes() {
_ = Self.reportConfigured
let failures = assertSnapshot(
of: LoginViewController(),
highContrastReport: false,
referenceURL: "https://app.zeplin.io/project/abc/screen/login"
)
#expect(failures.isEmpty)
}
@Test("login screen all appearance modes")
func loginAllModes() {
_ = Self.reportConfigured
let failures = assertSnapshot(
of: LoginViewController(),
captureHeight: .complete,
highContrastReport: true
)
#expect(failures.isEmpty)
}
}import XCTest
import SnapshotTesting
import SnapshotReportTesting
final class LoginSnapshotsTests: XCTestCase {
override func setUp() {
super.setUp()
configureSnapshotReport(
reportName: "iOS Snapshot Tests",
outputJSONPath: ".artifacts/snapshot-run.json",
metadata: ["platform": "iOS", "suite": "Login"]
)
}
func test_login_screen() {
// Default: light + dark snapshots from a single assert.
assertSnapshot(
of: LoginViewController(),
device: .iPhoneSe,
referenceURL: "https://www.figma.com/file/abc/Login?node-id=1"
)
}
func test_login_screen_all_contrasts() {
assertSnapshot(
of: LoginViewController(),
device: .iPhoneSe,
appearances: SnapshotAppearanceConfiguration.all
)
}
}assertSnapshot configuration highlights:
device: validates compatibility against current iOS major version (can override withosMajorVersion).configuredOSMajorVersion: a single configured runtime major version (default from global assertion defaults).- Global defaults are centralized with
configureSnapshotAssertionDefaults(...), and per-assert overrides remain optional. captureHeight: choose.device,.large,.complete, or.points(Double)for taller captures.highContrastReport: trueforces high-contrast variants and uses order: high contrast light, light, dark, high contrast dark.missingReferencePolicy: defaults to.recordOnMissingReference(auto-record if asset is missing).diffing: defaults toCoreImageDifferenceDiffing()and attaches an advanced diff PNG on failures. The actual image is also always attached soodiffcan run at CLI time.referenceURL: optional design reference URL (Zeplin, Figma, etc.) rendered as a link in the HTML report.
You can still use any SnapshotTesting strategy and record it in the report:
assertReportingSnapshot(
of: value,
as: .json,
referenceURL: "https://app.zeplin.io/project/abc/screen/123"
)After tests generate one or more run JSON files:
# Basic
swift run snapshot-report \
--input .artifacts/snapshot-run.json \
--output .artifacts/report \
--format json,junit,html
# With odiff (auto-detected on PATH, or specify --odiff /path/to/odiff)
swift run snapshot-report \
--input-dir .artifacts/snapshot-runs \
--output .artifacts/report \
--format json,junit,html
# From xcresult (no custom assertion layer required)
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-resultBundlePath .artifacts/xcresult/MyApp.xcresult
swift run snapshot-report \
--xcresult .artifacts/xcresult/MyApp.xcresult \
--output .artifacts/report \
--format json,junit,html# Pass snapshot catalog from multiple xcresult bundles
swift run snapshot-report \
--xcresult .artifacts/xcresult/UIKitSnapshots-pass.xcresult \
--xcresult .artifacts/xcresult/SwiftUISnapshots-pass.xcresult \
--output .artifacts/review-report-pass \
--format json,html \
--name "UIKit + SwiftUI Snapshot Catalog"When pass attachments are missing in xcresult, the CLI attempts to resolve matching references from local __Snapshots__/ folders and includes them in the report.
Default template is bundled at:
Sources/SnapshotReportCore/Resources/default-report.stencil
You can override with --html-template and use these context keys:
report.namereport.generatedAtreport.summary.total|passed|failed|skipped|durationsuites[]suite.tests[]test.status,test.name,test.className,test.durationtest.referenceURL— design reference URL (empty string if not set)test.failure.message|file|line|difftest.attachments[].name|type|path|contenttest.failedGroups[]— grouped failed entries withgroupName,snapshot,diff,failuretest.passedGroups[]— grouped passed entries withgroupName,attachments[]
Attachment names produced automatically by the assertion layer:
| Name | Description |
|---|---|
Snapshot |
Reference image from __Snapshots__/ |
Actual Snapshot |
Image captured during the failing test run |
Advanced Diff |
CoreImage difference blend (when diffing is provided) |
odiff |
SIMD-accelerated diff from the odiff binary (added at CLI time) |
Failure Message |
Plain-text failure message (when no diff text is embedded) |
| Variable | Effect |
|---|---|
SNAPSHOT_REPORT_OUTPUT_DIR |
Directory for per-run JSON files; a unique filename is generated per process |
SNAPSHOT_REPORT_OUTPUT |
Explicit full path for the output JSON |
SNAPSHOT_REPORT_NAME |
Report name embedded in the JSON |
SRCROOT |
Fallback root; output defaults to $SRCROOT/.artifacts/snapshot-runs/ |
SCHEME_NAME, GIT_BRANCH, TEST_PLAN_NAME, TARGET_NAME |
Auto-populated into report metadata |
Use snapshot-report inspect --gitlab to generate a tailored snippet, or start from this template:
snapshot-tests:
stage: test
script:
- xcodebuild test
-project MyApp.xcodeproj
-scheme MyApp
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest'
SNAPSHOT_REPORT_OUTPUT_DIR=$CI_PROJECT_DIR/.artifacts/snapshot-runs
SRCROOT=$CI_PROJECT_DIR
GIT_BRANCH=$CI_COMMIT_REF_NAME
SCHEME_NAME=MyApp
- swift run snapshot-report
--input-dir .artifacts/snapshot-runs
--output .artifacts/snapshot-report
--format json,junit,html
artifacts:
paths:
- .artifacts/snapshot-runs/
- .artifacts/snapshot-report/
reports:
junit: .artifacts/snapshot-report/report.junit.xml
only:
- schedulesSchedule this pipeline in GitLab CI → Schedules to run nightly regression checks on your snapshot baselines.