Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Set up SwiftUI visual regression testing with swift-snapshot-testing. Generates snapshot test boilerplate and CI configuration. Use for UI regression prevention.
.claude/skills/rshankras-snapshot-test-setup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 206% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 84% | 0% |
Generate SwiftUI snapshot/visual regression tests using Point-Free's swift-snapshot-testing library. Catches unintended UI changes by comparing rendered views against reference images.
Use this skill when the user:
Without snapshots: With snapshots:
Change a modifier Change a modifier
→ Looks fine locally → Snapshot test fails
→ Push to main → Shows exact visual diff
→ User reports UI bug → Fix before merging
→ Embarrassing → Confidence in UI changesGlob: **/Package.swift or **/*.xcodeproj
Grep: "swift-snapshot-testing" (already added?)
Grep: "SnapshotTesting" in test filesAsk via AskUserQuestion:
swift// Package.swift dependencies: [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0" ) ] // Test target .testTarget( name: "YourAppTests", dependencies: [ "YourApp", .product(name: "SnapshotTesting", package: "swift-snapshot-testing") ] )
https://github.com/pointfreeco/swift-snapshot-testingSnapshotTesting to your test targetswiftimport Testing import SnapshotTesting import SwiftUI @testable import YourApp // MARK: - Snapshot Configuration enum SnapshotConfig { // iOS devices to test static let iPhoneConfigs: [String: ViewImageConfig] = [ "iPhone_SE": .iPhoneSe, "iPhone_16": .iPhone13, // Similar dimensions "iPhone_16_Pro_Max": .iPhone13ProMax ] // macOS window sizes static let macOSConfigs: [String: CGSize] = [ "compact": CGSize(width: 400, height: 600), "regular": CGSize(width: 800, height: 600), "wide": CGSize(width: 1200, height: 800) ] // Color schemes to test static let colorSchemes: [ColorScheme] = [.light, .dark] }
swift@Suite("Snapshots: HomeView") struct HomeViewSnapshotTests { // perceptualPrecision < 1.0 absorbs GPU/anti-aliasing noise across runs // on the same pinned simulator — 0.98 catches real layout/color changes // while ignoring sub-perceptual rendering jitter. @Test("matches reference - light mode") func lightMode() { let view = HomeView(items: Item.sampleList) assertSnapshot( of: UIHostingController(rootView: view), as: .image(on: .iPhone13, perceptualPrecision: 0.98) ) } @Test("matches reference - dark mode") func darkMode() { let view = HomeView(items: Item.sampleList) .environment(\.colorScheme, .dark) assertSnapshot( of: UIHostingController(rootView: view), as: .image(on: .iPhone13, perceptualPrecision: 0.98) ) } @Test("matches reference - empty state") func emptyState() { let view = HomeView(items: []) assertSnapshot( of: UIHostingController(rootView: view), as: .image(on: .iPhone13, perceptualPrecision: 0.98) ) } @Test("matches reference - accessibility Dynamic Type") func dynamicTypeAccessibility() { let view = HomeView(items: Item.sampleList) .environment(\.dynamicTypeSize, .accessibility3) assertSnapshot( of: UIHostingController(rootView: view), as: .image(on: .iPhone13, perceptualPrecision: 0.98) ) } }
swift@Suite("Snapshots: SettingsView") struct SettingsViewSnapshotTests { @Test("matches reference - standard size") func standardSize() { let view = SettingsView() .frame(width: 500, height: 400) assertSnapshot( of: NSHostingController(rootView: view), as: .image(size: CGSize(width: 500, height: 400)) ) } @Test("matches reference - dark mode") func darkMode() { let view = SettingsView() .frame(width: 500, height: 400) .environment(\.colorScheme, .dark) assertSnapshot( of: NSHostingController(rootView: view), as: .image(size: CGSize(width: 500, height: 400)) ) } }
swift@Suite("Snapshots: ItemCard") struct ItemCardSnapshotTests { @Test("default state") func defaultState() { let view = ItemCard(item: .sample) .frame(width: 300) assertSnapshot(of: view, as: .image) } @Test("selected state") func selectedState() { let view = ItemCard(item: .sample, isSelected: true) .frame(width: 300) assertSnapshot(of: view, as: .image) } @Test("long title wraps") func longTitle() { let item = Item(title: "This is a very long title that should wrap to multiple lines") let view = ItemCard(item: item) .frame(width: 300) assertSnapshot(of: view, as: .image) } }
First run records reference images (golden masters):
bash# Record all snapshots (first run) xcodebuild test -scheme YourApp \ -destination 'platform=iOS Simulator,name=iPhone 16'
Reference images land in __Snapshots__/ directories next to test files:
Tests/SnapshotTests/
├── __Snapshots__/
│ └── HomeViewSnapshotTests/
│ ├── lightMode.1.png
│ ├── darkMode.1.png
│ ├── emptyState.1.png
│ └── dynamicTypeXXL.1.png
├── HomeViewSnapshotTests.swift
└── ItemCardSnapshotTests.swiftWhen you intentionally change a view:
swift// Temporarily set record mode @Test("matches reference - light mode") func lightMode() { withSnapshotTesting(record: .all) { let view = HomeView(items: Item.sampleList) assertSnapshot( of: UIHostingController(rootView: view), as: .image(on: .iPhone13) ) } }
Snapshot suites are the UI half of the deterministic gauntlet (code half: testing/fitness-functions, swift/code-size, testing/coverage-ratchet). They use the same install trick as fitness functions: they are ordinary tests in the existing unit-test target, so they ride every test gate that already exists — no new CI plumbing to run them. What makes them a gate rather than a capability:
type="snapshot"> runs the suite scoped to a task's touched screens (-only-testing); the phase does not close on a red diff.
__Snapshots__/ baselines are committed;re-recording is a stated decision — the commit message says which screens changed and why. Record mode (withSnapshotTesting(record: .all)) is never committed enabled; a committed record-mode test asserts nothing.
destination and document it in the suite header (baselines from other devices/OS versions will diff). Use perceptualPrecision: 0.98. Require a double-run green before committing new baselines — a suite that flakes between two identical runs is not a gate. OS/simulator bumps legitimately require re-records: that's a stated-intent event like any other.
a snapshot for its touched screens leaves the gate blind exactly where the change happened.
its #Previews already use to inject state/environment) rather than building a parallel one — the DEBUG-only harness is visible to test builds, and reuse keeps snapshots rendering exactly what previews render.
yaml- name: Run Snapshot Tests run: | xcodebuild test \ -scheme YourApp \ -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \ -only-testing "YourAppTests/Snapshots" \ -resultBundlePath TestResults.xcresult - name: Upload Failed Snapshots if: failure() uses: actions/upload-artifact@v4 with: name: failed-snapshots path: "**/Failures/**"
bash# ci_scripts/ci_post_xcodebuild.sh if [ "$CI_XCODEBUILD_ACTION" = "test" ]; then # Upload snapshot failures as artifacts if [ -d "$CI_DERIVED_DATA_PATH" ]; then find "$CI_DERIVED_DATA_PATH" -name "Failures" -type d \ -exec cp -r {} "$CI_RESULT_BUNDLE_PATH/" \; fi fi
markdown## Snapshot Tests Setup ### Dependency Added swift-snapshot-testing 1.17.0 via SPM ### Tests Generated | View | Configurations | Tests | |------|---------------|-------| | HomeView | light, dark, empty, XXL type | 4 | | SettingsView | light, dark | 2 | | ItemCard | default, selected, long title | 3 | | **Total** | | **9** | ### Files Created - `Tests/SnapshotTests/HomeViewSnapshotTests.swift` - `Tests/SnapshotTests/SettingsViewSnapshotTests.swift` - `Tests/SnapshotTests/ItemCardSnapshotTests.swift` ### Next Steps 1. Run tests once to record reference images 2. Commit `__Snapshots__/` directories to git 3. Add snapshot test step to CI pipeline
generators/test-generator/ — for unit/integration test generationtesting/tdd-feature/ — for TDD workflow with UI featurestesting/fitness-functions/ — the code half of the deterministic gauntlet (same ride-the-test-gate trick)Other measured skills in the registry, with their headline benchmark lift.