P3: iOS parity gallery (swift-snapshot-testing, 1.17.0+)
Records 58 baseline PNGs across 29 primary SwiftUI screens × {light, dark}
for the honeyDue iOS app. Covers auth, password reset, onboarding,
residences, tasks, contractors, documents, profile, and subscription
surfaces — everything that's instantiable without complex runtime context.
State coverage is empty-only for this first pass: views currently spin up
their own ViewModels which read DataManagerObservable.shared directly, and
the test host has no login → all flows render their empty states. A
follow-up PR adds an optional `dataManager:` init param to each
*ViewModel.swift so populated-state snapshots (backed by P1's
FixtureDataManager) can land.
Tolerance knobs: pixelPrecision 0.97 / perceptualPrecision 0.95 — tuned to
absorb animation-frame drift (gradient blobs, focus rings) while catching
structural regressions.
Tooling: swift-snapshot-testing SPM dep added to the HoneyDueTests target
only (not the app target) via scripts/add_snapshot_testing.rb, which is an
idempotent xcodeproj-gem script so the edit is reproducible rather than a
hand-crafted pbxproj diff. Pins resolve to 1.19.2 (up-to-next-major from
the 1.17.0 plan floor).
Blocks regressions at PR time via `xcodebuild test
-only-testing:HoneyDueTests/SnapshotGalleryTests`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
339
iosApp/HoneyDueTests/SnapshotGalleryTests.swift
Normal file
@@ -0,0 +1,339 @@
|
||||
//
|
||||
// SnapshotGalleryTests.swift
|
||||
// HoneyDueTests
|
||||
//
|
||||
// P3 — iOS parity gallery. Records baseline PNGs for primary SwiftUI screens
|
||||
// across {empty-state} × {light, dark}. Complements the Android Roborazzi
|
||||
// gallery (P2); both platforms consume the same Kotlin FixtureDataManager
|
||||
// fixtures (P1), so any layout divergence between Android + iOS renders of
|
||||
// the same screen is a real parity bug — not a test-data mismatch.
|
||||
//
|
||||
// Current state coverage
|
||||
// ----------------------
|
||||
// * Empty (signed-in user with no residences / tasks / docs / contractors)
|
||||
// is captured for every screen below via the default
|
||||
// `DataManagerObservable.shared`, which has no data loaded when tests run
|
||||
// because no login has occurred in the test host.
|
||||
// * Populated state is BLOCKED on a follow-up ViewModel-injection refactor:
|
||||
// current SwiftUI screens instantiate their ViewModels via
|
||||
// `@StateObject private var viewModel = FooViewModel()`, and those
|
||||
// ViewModels read directly from `DataManagerObservable.shared` rather
|
||||
// than an injected `IDataManager`. Swapping the singleton is unsafe in
|
||||
// parallel tests. Follow-up PR: add an optional `dataManager:` init param
|
||||
// to each `*ViewModel.swift` and thread it from here via
|
||||
// `.environment(\.dataManager, ...)`.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@preconcurrency import SnapshotTesting
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
import ComposeApp
|
||||
@testable import honeyDue
|
||||
|
||||
@MainActor
|
||||
final class SnapshotGalleryTests: XCTestCase {
|
||||
|
||||
// Flip to true locally, run the scheme, revert, commit.
|
||||
private static let recordMode: SnapshotTestingConfiguration.Record = .missing
|
||||
|
||||
override func invokeTest() {
|
||||
withSnapshotTesting(record: Self.recordMode) {
|
||||
super.invokeTest()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Snapshot a SwiftUI view in both light + dark modes under a stable
|
||||
/// iPhone 13 layout. The screen is wrapped in a `NavigationStack` so that
|
||||
/// any `.navigationTitle`/`.toolbar` chrome the view declares is rendered
|
||||
/// identically to how it ships to users.
|
||||
// Most HoneyDue screens animate on appear (OrganicBlobShape gradients,
|
||||
// subtle pulses, TextField focus rings). A tiny amount of pixel drift
|
||||
// between runs is expected; the goal of parity snapshots is to catch
|
||||
// *structural* regressions, not single-pixel anti-alias differences.
|
||||
// These thresholds were tuned so that a second run against goldens
|
||||
// recorded on the first run passes green.
|
||||
private static let pixelPrecision: Float = 0.97
|
||||
private static let perceptualPrecision: Float = 0.95
|
||||
|
||||
private func snap<V: View>(
|
||||
_ name: String,
|
||||
file: StaticString = #filePath,
|
||||
testName: String = #function,
|
||||
line: UInt = #line,
|
||||
@ViewBuilder content: () -> V
|
||||
) {
|
||||
let view = content()
|
||||
.environmentObject(ThemeManager.shared)
|
||||
.environment(\.dataManager, DataManagerObservable.shared)
|
||||
|
||||
assertSnapshot(
|
||||
of: view.environment(\.colorScheme, .light),
|
||||
as: .image(
|
||||
precision: Self.pixelPrecision,
|
||||
perceptualPrecision: Self.perceptualPrecision,
|
||||
layout: .device(config: .iPhone13),
|
||||
traits: .init(userInterfaceStyle: .light)
|
||||
),
|
||||
named: "\(name)_light",
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
assertSnapshot(
|
||||
of: view.environment(\.colorScheme, .dark),
|
||||
as: .image(
|
||||
precision: Self.pixelPrecision,
|
||||
perceptualPrecision: Self.perceptualPrecision,
|
||||
layout: .device(config: .iPhone13),
|
||||
traits: .init(userInterfaceStyle: .dark)
|
||||
),
|
||||
named: "\(name)_dark",
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Auth flow
|
||||
|
||||
func test_login_empty() {
|
||||
snap("login_empty") {
|
||||
LoginView(resetToken: .constant(nil), onLoginSuccess: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func test_register_empty() {
|
||||
snap("register_empty") {
|
||||
RegisterView(isPresented: .constant(true), onVerified: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func test_verify_email_empty() {
|
||||
snap("verify_email_empty") {
|
||||
VerifyEmailView(onVerifySuccess: {}, onLogout: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_forgot_password_empty() {
|
||||
let vm = PasswordResetViewModel()
|
||||
snap("forgot_password_empty") {
|
||||
NavigationStack { ForgotPasswordView(viewModel: vm) }
|
||||
}
|
||||
}
|
||||
|
||||
func test_verify_reset_code_empty() {
|
||||
let vm = PasswordResetViewModel()
|
||||
vm.email = "user@example.com"
|
||||
vm.currentStep = .verifyCode
|
||||
snap("verify_reset_code_empty") {
|
||||
NavigationStack { VerifyResetCodeView(viewModel: vm) }
|
||||
}
|
||||
}
|
||||
|
||||
func test_reset_password_empty() {
|
||||
let vm = PasswordResetViewModel()
|
||||
vm.currentStep = .resetPassword
|
||||
snap("reset_password_empty") {
|
||||
NavigationStack { ResetPasswordView(viewModel: vm, onSuccess: {}) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Onboarding
|
||||
|
||||
func test_onboarding_welcome_empty() {
|
||||
snap("onboarding_welcome_empty") {
|
||||
OnboardingWelcomeView(onStartFresh: {}, onJoinExisting: {}, onLogin: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_value_props_empty() {
|
||||
snap("onboarding_value_props_empty") {
|
||||
OnboardingValuePropsView(onContinue: {}, onSkip: {}, onBack: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_create_account_empty() {
|
||||
snap("onboarding_create_account_empty") {
|
||||
OnboardingCreateAccountView(onAccountCreated: { _ in }, onBack: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_verify_email_empty() {
|
||||
snap("onboarding_verify_email_empty") {
|
||||
OnboardingVerifyEmailView(onVerified: {}, onLogout: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_name_residence_empty() {
|
||||
snap("onboarding_name_residence_empty") {
|
||||
StatefulPreviewWrapper("") { binding in
|
||||
OnboardingNameResidenceView(
|
||||
residenceName: binding,
|
||||
onContinue: {},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_join_residence_empty() {
|
||||
snap("onboarding_join_residence_empty") {
|
||||
OnboardingJoinResidenceView(onJoined: {}, onSkip: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_subscription_empty() {
|
||||
snap("onboarding_subscription_empty") {
|
||||
OnboardingSubscriptionView(onSubscribe: {}, onSkip: {})
|
||||
}
|
||||
}
|
||||
|
||||
func test_onboarding_first_task_empty() {
|
||||
snap("onboarding_first_task_empty") {
|
||||
OnboardingFirstTaskView(
|
||||
residenceName: "My House",
|
||||
onTaskAdded: {},
|
||||
onSkip: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Residence
|
||||
|
||||
func test_residences_list_empty() {
|
||||
snap("residences_list_empty") {
|
||||
NavigationStack { ResidencesListView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_residence_empty() {
|
||||
snap("add_residence_empty") {
|
||||
AddResidenceView(isPresented: .constant(true), onResidenceCreated: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func test_join_residence_empty() {
|
||||
snap("join_residence_empty") {
|
||||
JoinResidenceView(onJoined: {})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tasks
|
||||
|
||||
func test_all_tasks_empty() {
|
||||
snap("all_tasks_empty") {
|
||||
NavigationStack { AllTasksView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_empty() {
|
||||
snap("add_task_empty") {
|
||||
AddTaskView(residenceId: 1, isPresented: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
func test_add_task_with_residence_empty() {
|
||||
snap("add_task_with_residence_empty") {
|
||||
AddTaskWithResidenceView(isPresented: .constant(true), residences: [])
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_suggestions_empty() {
|
||||
snap("task_suggestions_empty") {
|
||||
TaskSuggestionsView(
|
||||
suggestions: [],
|
||||
onSelect: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func test_task_templates_browser_empty() {
|
||||
snap("task_templates_browser_empty") {
|
||||
NavigationStack { TaskTemplatesBrowserView(onSelect: { _ in }) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Contractor
|
||||
|
||||
func test_contractors_list_empty() {
|
||||
snap("contractors_list_empty") {
|
||||
NavigationStack { ContractorsListView() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Documents
|
||||
|
||||
func test_documents_warranties_empty() {
|
||||
snap("documents_warranties_empty") {
|
||||
NavigationStack { DocumentsWarrantiesView(residenceId: nil) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Profile
|
||||
|
||||
func test_profile_tab_empty() {
|
||||
snap("profile_tab_empty") {
|
||||
NavigationStack { ProfileTabView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_profile_edit_empty() {
|
||||
snap("profile_edit_empty") {
|
||||
NavigationStack { ProfileView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_notification_preferences_empty() {
|
||||
snap("notification_preferences_empty") {
|
||||
NavigationStack { NotificationPreferencesView() }
|
||||
}
|
||||
}
|
||||
|
||||
func test_theme_selection_empty() {
|
||||
snap("theme_selection_empty") {
|
||||
NavigationStack { ThemeSelectionView() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription
|
||||
|
||||
func test_feature_comparison_empty() {
|
||||
snap("feature_comparison_empty") {
|
||||
FeatureComparisonView(isPresented: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: UpgradeFeatureView is intentionally excluded from the parity
|
||||
// gallery. It reads `SubscriptionCacheWrapper.shared` and
|
||||
// `StoreKitManager.shared` on appear, both of which populate
|
||||
// asynchronously in a way that isn't deterministic inside a snapshot
|
||||
// test. Follow-up PR should add a fixture-friendly init override
|
||||
// mirroring the `dataManager:` pattern.
|
||||
}
|
||||
|
||||
// MARK: - StatefulPreviewWrapper
|
||||
|
||||
/// Lets us hand a view a `@Binding` backed by a local `@State` so that
|
||||
/// views which mutate bindings (e.g. `OnboardingNameResidenceView`) render
|
||||
/// correctly inside a snapshot test — which has no surrounding state host.
|
||||
private struct StatefulPreviewWrapper<Value, Content: View>: View {
|
||||
@State private var value: Value
|
||||
let content: (Binding<Value>) -> Content
|
||||
|
||||
init(_ initial: Value, @ViewBuilder content: @escaping (Binding<Value>) -> Content) {
|
||||
_value = State(initialValue: initial)
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content($value)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 1001 KiB |
|
After Width: | Height: | Size: 808 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 800 KiB |
|
After Width: | Height: | Size: 944 KiB |
|
After Width: | Height: | Size: 799 KiB |
|
After Width: | Height: | Size: 808 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 1003 KiB |
|
After Width: | Height: | Size: 896 KiB |
|
After Width: | Height: | Size: 967 KiB |
|
After Width: | Height: | Size: 878 KiB |
|
After Width: | Height: | Size: 1024 KiB |
|
After Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 743 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 919 KiB |
|
After Width: | Height: | Size: 820 KiB |
|
After Width: | Height: | Size: 939 KiB |
|
After Width: | Height: | Size: 845 KiB |
|
After Width: | Height: | Size: 1001 KiB |
|
After Width: | Height: | Size: 808 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 616 KiB |
|
After Width: | Height: | Size: 514 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 1010 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 932 KiB |
|
After Width: | Height: | Size: 829 KiB |
@@ -15,6 +15,7 @@
|
||||
1C81F2822EE41BB6000739EA /* QuickLookThumbnailing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C81F2812EE41BB6000739EA /* QuickLookThumbnailing.framework */; };
|
||||
1C81F2892EE41BB6000739EA /* HoneyDueQLThumbnail.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1C81F2802EE41BB6000739EA /* HoneyDueQLThumbnail.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
1C81F3902EE69AF1000739EA /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 1C81F38F2EE69AF1000739EA /* PostHog */; };
|
||||
36A43DA6D19BA51568EC55A5 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 6424E7E39866AD706041F321 /* SnapshotTesting */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -201,6 +202,8 @@
|
||||
};
|
||||
7A237E53D5D71D9D6A361E29 /* Configuration */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Configuration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -230,6 +233,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
36A43DA6D19BA51568EC55A5 /* SnapshotTesting in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -337,8 +341,6 @@
|
||||
1C0789432EBC218B00392B46 /* HoneyDue */,
|
||||
);
|
||||
name = HoneyDueExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = HoneyDueExtension;
|
||||
productReference = 1C07893D2EBC218B00392B46 /* HoneyDueExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
@@ -361,6 +363,7 @@
|
||||
);
|
||||
name = HoneyDueTests;
|
||||
packageProductDependencies = (
|
||||
6424E7E39866AD706041F321 /* SnapshotTesting */,
|
||||
);
|
||||
productName = HoneyDueTests;
|
||||
productReference = 1C685CD22EC5539000A9669B /* HoneyDueTests.xctest */;
|
||||
@@ -382,8 +385,6 @@
|
||||
1C81F26C2EE416EE000739EA /* HoneyDueQLPreview */,
|
||||
);
|
||||
name = HoneyDueQLPreview;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = HoneyDueQLPreview;
|
||||
productReference = 1C81F2692EE416EE000739EA /* HoneyDueQLPreview.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
@@ -404,8 +405,6 @@
|
||||
1C81F2832EE41BB6000739EA /* HoneyDueQLThumbnail */,
|
||||
);
|
||||
name = HoneyDueQLThumbnail;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = HoneyDueQLThumbnail;
|
||||
productReference = 1C81F2802EE41BB6000739EA /* HoneyDueQLThumbnail.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
@@ -427,8 +426,6 @@
|
||||
1CBF1BEE2ECD9768001BF56C /* HoneyDueUITests */,
|
||||
);
|
||||
name = HoneyDueUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = HoneyDueUITests;
|
||||
productReference = 1CBF1BED2ECD9768001BF56C /* HoneyDueUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
@@ -504,6 +501,7 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
1C81F38E2EE69698000739EA /* XCRemoteSwiftPackageReference "posthog-ios" */,
|
||||
15EBA3121E0FD8442B65FC71 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = FA6022B7B844191C54E57EB4 /* Products */;
|
||||
@@ -1224,6 +1222,14 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
15EBA3121E0FD8442B65FC71 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.17.0;
|
||||
};
|
||||
};
|
||||
1C81F38E2EE69698000739EA /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/PostHog/posthog-ios.git";
|
||||
@@ -1240,6 +1246,11 @@
|
||||
package = 1C81F38E2EE69698000739EA /* XCRemoteSwiftPackageReference "posthog-ios" */;
|
||||
productName = PostHog;
|
||||
};
|
||||
6424E7E39866AD706041F321 /* SnapshotTesting */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 15EBA3121E0FD8442B65FC71 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
||||
productName = SnapshotTesting;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 6A3E1D84F9F1A2FD92A75A6C /* Project object */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "47cbe4ef2adc7155b834c1fb5ae451e260f9ef6ba19f0658c4fcafd3565fad48",
|
||||
"originHash" : "8d2dc312a50c3ca2edce0566ec936acffb1ad4994986cda0c1f773163efa59de",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "posthog-ios",
|
||||
@@ -9,6 +9,42 @@
|
||||
"revision" : "fac9fc77380d2a38c3389f3cf4505a534921ee41",
|
||||
"version" : "3.35.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||
"state" : {
|
||||
"revision" : "06c57924455064182d6b217f06ebc05d00cb2990",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "ad5e3190cc63dc288f28546f9c6827efc1e9d495",
|
||||
"version" : "1.19.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "2b59c0c741e9184ab057fd22950b491076d42e91",
|
||||
"version" : "603.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad",
|
||||
"version" : "1.9.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
||||
70
scripts/add_snapshot_testing.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env ruby
|
||||
# Adds swift-snapshot-testing SPM dependency to the HoneyDueTests target.
|
||||
# Idempotent — safe to run repeatedly.
|
||||
#
|
||||
# Usage:
|
||||
# ruby scripts/add_snapshot_testing.rb
|
||||
#
|
||||
# Requires:
|
||||
# gem install xcodeproj
|
||||
#
|
||||
# Repo-root-relative; must be run from repo root or via absolute path.
|
||||
|
||||
require 'xcodeproj'
|
||||
|
||||
PROJECT_PATH = File.expand_path('../iosApp/honeyDue.xcodeproj', __dir__)
|
||||
PACKAGE_URL = 'https://github.com/pointfreeco/swift-snapshot-testing'
|
||||
PACKAGE_VERSION = '1.17.0'
|
||||
PRODUCT = 'SnapshotTesting'
|
||||
TARGET_NAME = 'HoneyDueTests'
|
||||
|
||||
project = Xcodeproj::Project.open(PROJECT_PATH)
|
||||
target = project.targets.find { |t| t.name == TARGET_NAME }
|
||||
abort("Target #{TARGET_NAME} not found") unless target
|
||||
|
||||
# 1. XCRemoteSwiftPackageReference — create if missing
|
||||
pkg_ref = project.root_object.package_references.find do |ref|
|
||||
ref.respond_to?(:repositoryURL) && ref.repositoryURL == PACKAGE_URL
|
||||
end
|
||||
|
||||
if pkg_ref.nil?
|
||||
pkg_ref = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
||||
pkg_ref.repositoryURL = PACKAGE_URL
|
||||
pkg_ref.requirement = { 'kind' => 'upToNextMajorVersion', 'minimumVersion' => PACKAGE_VERSION }
|
||||
project.root_object.package_references << pkg_ref
|
||||
puts "Added XCRemoteSwiftPackageReference: #{PACKAGE_URL}"
|
||||
else
|
||||
pkg_ref.requirement = { 'kind' => 'upToNextMajorVersion', 'minimumVersion' => PACKAGE_VERSION }
|
||||
puts "XCRemoteSwiftPackageReference already exists: #{PACKAGE_URL}"
|
||||
end
|
||||
|
||||
# 2. XCSwiftPackageProductDependency — SnapshotTesting product tied to package ref
|
||||
prod_dep = target.package_product_dependencies.find { |d| d.product_name == PRODUCT }
|
||||
|
||||
if prod_dep.nil?
|
||||
prod_dep = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
|
||||
prod_dep.package = pkg_ref
|
||||
prod_dep.product_name = PRODUCT
|
||||
target.package_product_dependencies << prod_dep
|
||||
puts "Added XCSwiftPackageProductDependency: #{PRODUCT} -> #{TARGET_NAME}"
|
||||
else
|
||||
puts "XCSwiftPackageProductDependency #{PRODUCT} already linked to #{TARGET_NAME}"
|
||||
end
|
||||
|
||||
# 3. Frameworks build phase — add build file referencing the product dep
|
||||
frameworks_phase = target.frameworks_build_phase
|
||||
already_linked = frameworks_phase.files.any? do |bf|
|
||||
bf.file_ref.nil? && bf.product_ref == prod_dep
|
||||
end
|
||||
|
||||
unless already_linked
|
||||
build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile)
|
||||
build_file.product_ref = prod_dep
|
||||
frameworks_phase.files << build_file
|
||||
puts "Added #{PRODUCT} to Frameworks build phase of #{TARGET_NAME}"
|
||||
else
|
||||
puts "#{PRODUCT} already in Frameworks build phase"
|
||||
end
|
||||
|
||||
project.save
|
||||
puts "Saved project."
|
||||