mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-31 04:52:05 +00:00
Separate AppContext for previews and UI testing (#961)
Clarify the use of contexts: - **Production** (.shared) - **Previews** (.mock → .forPreviews) - ONLY use it in UILibrary for, well, previews - This context has dumb profiles with UUIDs as names - Registry is fake - **UI Tests** (.forUITesting) - Add new context for UI testing - Selected based on command line arguments - This context has mock data tuned for decent screenshots - Registry is real Share the same InAppProcessor in .shared and .forTesting contexts because the app behavior was inconsistent regarding e.g. in-app purchases.
This commit is contained in:
parent
962361cb9f
commit
2a467e0c7e
@ -16,6 +16,8 @@
|
||||
0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */; };
|
||||
0E483E842CE6501100584B32 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E822CE6501100584B32 /* Shared+App.swift */; };
|
||||
0E60512C2CE5393C00F763D4 /* PassepartoutImplementations in Frameworks */ = {isa = PBXBuildFile; productRef = 0E60512B2CE5393C00F763D4 /* PassepartoutImplementations */; };
|
||||
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */; };
|
||||
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */; };
|
||||
0E757F132CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E757F122CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift */; };
|
||||
0E757F202CD0D22B006E13E1 /* PassepartoutLoginItem.app in Embed Login Item */ = {isa = PBXBuildFile; fileRef = 0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
0E757F232CD0D2BD006E13E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E757F212CD0D2B7006E13E1 /* AppDelegate.swift */; };
|
||||
@ -134,6 +136,8 @@
|
||||
0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+Tunnel.swift"; sourceTree = "<group>"; };
|
||||
0E483E822CE6501100584B32 /* Shared+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+App.swift"; sourceTree = "<group>"; };
|
||||
0E5DFDDC2CDB8F9100F2DE70 /* Passepartout.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.storekit; sourceTree = "<group>"; };
|
||||
0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+Testing.swift"; sourceTree = "<group>"; };
|
||||
0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileManager+Testing.swift"; sourceTree = "<group>"; };
|
||||
0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PassepartoutLoginItem.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0E757F122CD0CFFC006E13E1 /* PassepartoutLoginItemApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassepartoutLoginItemApp.swift; sourceTree = "<group>"; };
|
||||
0E757F182CD0CFFD006E13E1 /* LoginItem.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoginItem.entitlements; sourceTree = "<group>"; };
|
||||
@ -277,6 +281,15 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E6EEEE62CF8CB090076E2B0 /* Testing */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */,
|
||||
0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */,
|
||||
);
|
||||
path = Testing;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E757F112CD0CFFC006E13E1 /* LoginItem */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -323,6 +336,7 @@
|
||||
0E7E3D612B9345FD002BBDB4 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E6EEEE62CF8CB090076E2B0 /* Testing */,
|
||||
0EC797402B9378E000C093B7 /* AppContext+Shared.swift */,
|
||||
0EC797412B9378E000C093B7 /* Shared.swift */,
|
||||
0E483E822CE6501100584B32 /* Shared+App.swift */,
|
||||
@ -673,6 +687,8 @@
|
||||
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */,
|
||||
0EC797422B9378E000C093B7 /* AppContext+Shared.swift in Sources */,
|
||||
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
|
||||
0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */,
|
||||
0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */,
|
||||
0E483E842CE6501100584B32 /* Shared+App.swift in Sources */,
|
||||
0EC797432B9378E000C093B7 /* Shared.swift in Sources */,
|
||||
);
|
||||
|
@ -32,9 +32,9 @@ import UITesting
|
||||
@MainActor
|
||||
final class AppDelegate: NSObject {
|
||||
let context: AppContext = {
|
||||
guard !AppCommandLine.contains(.uiTesting) else {
|
||||
if AppCommandLine.contains(.uiTesting) {
|
||||
pp_log(.app, .info, "UI tests: mock AppContext")
|
||||
return .mock(withRegistry: .shared)
|
||||
return .forUITesting(withRegistry: .shared)
|
||||
}
|
||||
return .shared
|
||||
}()
|
||||
|
@ -133,6 +133,20 @@
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "UITesting"
|
||||
BuildableName = "UITesting"
|
||||
BlueprintName = "UITesting"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
|
@ -165,8 +165,8 @@ extension AboutCoordinator {
|
||||
|
||||
#Preview {
|
||||
AboutCoordinator(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews
|
||||
)
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ extension AppCoordinator {
|
||||
}
|
||||
|
||||
var overriddenLayout: ProfilesLayout {
|
||||
guard !isUITesting else {
|
||||
if isUITesting {
|
||||
return isBigDevice ? .grid : .list
|
||||
}
|
||||
return layout
|
||||
@ -306,8 +306,8 @@ extension AppCoordinator {
|
||||
|
||||
#Preview {
|
||||
AppCoordinator(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
registry: Registry()
|
||||
)
|
||||
.withMockEnvironment()
|
||||
|
@ -110,7 +110,7 @@ private extension AppToolbar {
|
||||
Text("AppToolbar")
|
||||
.toolbar {
|
||||
AppToolbar(
|
||||
profileManager: .mock,
|
||||
profileManager: .forPreviews,
|
||||
registry: Registry(),
|
||||
layout: .constant(.list),
|
||||
isImporting: .constant(false),
|
||||
|
@ -123,7 +123,7 @@ private extension InstalledProfileView {
|
||||
style: .installedProfile,
|
||||
profileManager: profileManager,
|
||||
tunnel: tunnel,
|
||||
preview: .init(profile ?? .mock),
|
||||
preview: .init(profile ?? .forPreviews),
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler,
|
||||
flow: flow
|
||||
@ -298,9 +298,9 @@ private struct HeaderView: View {
|
||||
var body: some View {
|
||||
InstalledProfileView(
|
||||
layout: layout,
|
||||
profileManager: .mock,
|
||||
profile: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
profile: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default(),
|
||||
nextProfileId: .constant(nil)
|
||||
@ -313,9 +313,9 @@ private struct ContentView: View {
|
||||
ForEach(0..<3) { _ in
|
||||
ProfileRowView(
|
||||
style: .full,
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
preview: .init(.mock),
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
preview: .init(.forPreviews),
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default(),
|
||||
nextProfileId: .constant(nil),
|
||||
|
@ -70,7 +70,7 @@ struct OnboardingModifier: ViewModifier {
|
||||
|
||||
private extension OnboardingModifier {
|
||||
func advance() {
|
||||
guard !isUITesting else {
|
||||
if isUITesting {
|
||||
pp_log(.app, .info, "UI tests: skip onboarding")
|
||||
return
|
||||
}
|
||||
|
@ -67,13 +67,13 @@ struct ProfileCardView: View {
|
||||
Section {
|
||||
ProfileCardView(
|
||||
style: .compact,
|
||||
preview: .init(.mock)
|
||||
preview: .init(.forPreviews)
|
||||
)
|
||||
}
|
||||
Section {
|
||||
ProfileCardView(
|
||||
style: .full,
|
||||
preview: .init(.mock)
|
||||
preview: .init(.forPreviews)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -157,8 +157,8 @@ private struct PreviewView: View {
|
||||
NavigationStack {
|
||||
ProfileContainerView(
|
||||
layout: layout,
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
registry: Registry(),
|
||||
isImporting: .constant(false),
|
||||
errorHandler: .default()
|
||||
|
@ -158,9 +158,9 @@ private extension ProfileContextMenu {
|
||||
Menu("Menu") {
|
||||
ProfileContextMenu(
|
||||
style: .installedProfile,
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
preview: .init(.mock),
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
preview: .init(.forPreviews),
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default()
|
||||
)
|
||||
|
@ -150,8 +150,8 @@ private extension ProfileGridView {
|
||||
|
||||
#Preview {
|
||||
ProfileGridView(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default()
|
||||
)
|
||||
|
@ -149,8 +149,8 @@ private extension ProfileListView {
|
||||
|
||||
#Preview {
|
||||
ProfileListView(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default()
|
||||
)
|
||||
|
@ -205,14 +205,14 @@ private extension ProfileRowView {
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
let profile: Profile = .mock
|
||||
let profileManager: ProfileManager = .mock
|
||||
let profile: Profile = .forPreviews
|
||||
let profileManager: ProfileManager = .forPreviews
|
||||
|
||||
return Form {
|
||||
ProfileRowView(
|
||||
style: .compact,
|
||||
profileManager: profileManager,
|
||||
tunnel: .mock,
|
||||
tunnel: .forPreviews,
|
||||
preview: .init(profile),
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default(),
|
||||
|
@ -187,7 +187,7 @@ private extension DiagnosticsView {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DiagnosticsView(profileManager: .mock, tunnel: .mock) {
|
||||
DiagnosticsView(profileManager: .forPreviews, tunnel: .forPreviews) {
|
||||
[
|
||||
.init(date: Date(), url: URL(string: "http://one.com")!),
|
||||
.init(date: Date().addingTimeInterval(-60), url: URL(string: "http://two.com")!),
|
||||
|
@ -60,8 +60,8 @@ private extension ModuleDetailView {
|
||||
|
||||
#Preview {
|
||||
ModuleDetailView(
|
||||
profileEditor: ProfileEditor(profile: .mock),
|
||||
moduleId: Profile.mock.modules.first?.id,
|
||||
profileEditor: ProfileEditor(profile: .forPreviews),
|
||||
moduleId: Profile.forPreviews.modules.first?.id,
|
||||
moduleViewFactory: DefaultModuleViewFactory(registry: Registry())
|
||||
)
|
||||
.withMockEnvironment()
|
||||
|
@ -164,7 +164,7 @@ private extension ProfileCoordinator {
|
||||
|
||||
#Preview {
|
||||
ProfileCoordinator(
|
||||
profileManager: .mock,
|
||||
profileManager: .forPreviews,
|
||||
profileEditor: ProfileEditor(profile: .newMockProfile()),
|
||||
initialModuleId: nil,
|
||||
registry: Registry(),
|
||||
|
@ -147,7 +147,7 @@ private extension ModuleListView {
|
||||
|
||||
#Preview {
|
||||
ModuleListView(
|
||||
profileEditor: ProfileEditor(profile: .mock),
|
||||
profileEditor: ProfileEditor(profile: .forPreviews),
|
||||
selectedModuleId: .constant(nil),
|
||||
errorModuleIds: .constant([]),
|
||||
paywallReason: .constant(nil)
|
||||
|
@ -136,8 +136,8 @@ private extension AppCoordinator {
|
||||
|
||||
#Preview {
|
||||
AppCoordinator(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
registry: Registry()
|
||||
)
|
||||
.withMockEnvironment()
|
||||
|
@ -231,7 +231,7 @@ private extension ActiveProfileView {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.task {
|
||||
try? await ProviderManager.mock.fetchIndex(from: [API.bundled])
|
||||
try? await ProviderManager.forPreviews.fetchIndex(from: [API.bundled])
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,7 +247,7 @@ private struct ContentPreview: View {
|
||||
var body: some View {
|
||||
ActiveProfileView(
|
||||
profile: profile,
|
||||
tunnel: .mock,
|
||||
tunnel: .forPreviews,
|
||||
isSwitching: $isSwitching,
|
||||
focusedField: $focusedField,
|
||||
interactiveManager: InteractiveManager(),
|
||||
|
@ -115,7 +115,7 @@ private extension ProfileListView {
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("List") {
|
||||
ContentPreview(profileManager: .mock)
|
||||
ContentPreview(profileManager: .forPreviews)
|
||||
}
|
||||
|
||||
#Preview("Empty") {
|
||||
@ -131,7 +131,7 @@ private struct ContentPreview: View {
|
||||
var body: some View {
|
||||
ProfileListView(
|
||||
profileManager: profileManager,
|
||||
tunnel: .mock,
|
||||
tunnel: .forPreviews,
|
||||
focusedField: $focusedField,
|
||||
interactiveManager: InteractiveManager(),
|
||||
errorHandler: .default()
|
||||
|
@ -190,8 +190,8 @@ private extension ProfileView {
|
||||
|
||||
#Preview("List") {
|
||||
ProfileView(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
profileManager: .forPreviews,
|
||||
tunnel: .forPreviews,
|
||||
errorHandler: .default(),
|
||||
showsSidePanel: true
|
||||
)
|
||||
@ -201,7 +201,7 @@ private extension ProfileView {
|
||||
#Preview("Empty") {
|
||||
ProfileView(
|
||||
profileManager: ProfileManager(profiles: []),
|
||||
tunnel: .mock,
|
||||
tunnel: .forPreviews,
|
||||
errorHandler: .default(),
|
||||
showsSidePanel: true
|
||||
)
|
||||
|
@ -145,7 +145,7 @@ private struct DetailView: View {
|
||||
// MARK: -
|
||||
|
||||
#Preview {
|
||||
SettingsView(tunnel: .mock)
|
||||
SettingsView(tunnel: .forPreviews)
|
||||
.themeNavigationStack()
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ extension ProfileManager {
|
||||
return
|
||||
}
|
||||
requiredFeatures = allProfiles.reduce(into: [:]) {
|
||||
guard let ineligible = processor.verify($1.value), !ineligible.isEmpty else {
|
||||
guard let ineligible = processor.requiredFeatures($1.value), !ineligible.isEmpty else {
|
||||
return
|
||||
}
|
||||
$0[$1.key] = ineligible
|
||||
|
@ -35,7 +35,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
|
||||
|
||||
private nonisolated let _preview: (Profile) -> ProfilePreview
|
||||
|
||||
private nonisolated let _verify: (IAPManager, Profile) -> Set<AppFeature>?
|
||||
private nonisolated let _requiredFeatures: (IAPManager, Profile) -> Set<AppFeature>?
|
||||
|
||||
private nonisolated let _willRebuild: (IAPManager, Profile.Builder) throws -> Profile.Builder
|
||||
|
||||
@ -46,7 +46,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
|
||||
title: @escaping (Profile) -> String,
|
||||
isIncluded: @escaping (IAPManager, Profile) -> Bool,
|
||||
preview: @escaping (Profile) -> ProfilePreview,
|
||||
verify: @escaping (IAPManager, Profile) -> Set<AppFeature>?,
|
||||
requiredFeatures: @escaping (IAPManager, Profile) -> Set<AppFeature>?,
|
||||
willRebuild: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
|
||||
willInstall: @escaping (IAPManager, Profile) throws -> Profile
|
||||
) {
|
||||
@ -54,7 +54,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
|
||||
_title = title
|
||||
_isIncluded = isIncluded
|
||||
_preview = preview
|
||||
_verify = verify
|
||||
_requiredFeatures = requiredFeatures
|
||||
_willRebuild = willRebuild
|
||||
_willInstall = willInstall
|
||||
}
|
||||
@ -75,8 +75,8 @@ extension InAppProcessor: ProfileProcessor {
|
||||
_preview(profile)
|
||||
}
|
||||
|
||||
public func verify(_ profile: Profile) -> Set<AppFeature>? {
|
||||
_verify(iapManager, profile)
|
||||
public func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
|
||||
_requiredFeatures(iapManager, profile)
|
||||
}
|
||||
|
||||
public func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
|
||||
|
@ -31,7 +31,7 @@ public protocol ProfileProcessor {
|
||||
|
||||
func preview(from profile: Profile) -> ProfilePreview
|
||||
|
||||
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
|
||||
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>?
|
||||
|
||||
func verify(_ profile: Profile) -> Set<AppFeature>?
|
||||
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ extension View {
|
||||
|
||||
public func withMockEnvironment() -> some View {
|
||||
task {
|
||||
try? await AppContext.mock.profileManager.observeLocal()
|
||||
try? await AppContext.forPreviews.profileManager.observeLocal()
|
||||
}
|
||||
.withEnvironment(from: .mock, theme: Theme())
|
||||
.withEnvironment(from: .forPreviews, theme: Theme())
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// AppContext+Mock.swift
|
||||
// AppContext+Previews.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 6/22/24.
|
||||
@ -23,25 +23,18 @@
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension AppContext {
|
||||
public static let mock: AppContext = .mock(withRegistry: Registry())
|
||||
|
||||
public static func mock(withRegistry registry: Registry) -> AppContext {
|
||||
public static let forPreviews: AppContext = {
|
||||
let iapManager = IAPManager(
|
||||
customUserLevel: .subscriber,
|
||||
inAppHelper: FakeAppProductHelper(),
|
||||
receiptReader: FakeAppReceiptReader(),
|
||||
betaChecker: TestFlightChecker(),
|
||||
unrestrictedFeatures: [
|
||||
.interactiveLogin,
|
||||
.onDemand
|
||||
],
|
||||
productsAtBuild: { _ in
|
||||
[]
|
||||
}
|
||||
@ -57,17 +50,23 @@ extension AppContext {
|
||||
preview: {
|
||||
$0.localizedPreview
|
||||
},
|
||||
verify: { _, _ in
|
||||
requiredFeatures: { _, _ in
|
||||
nil
|
||||
},
|
||||
willRebuild: { _, builder in
|
||||
builder
|
||||
},
|
||||
willInstall: { _, profile in
|
||||
try profile.withProviderModules()
|
||||
profile
|
||||
}
|
||||
)
|
||||
let profileManager: ProfileManager = .mock(withRegistry: registry, processor: processor)
|
||||
let profileManager = {
|
||||
let profiles: [Profile] = (0..<20)
|
||||
.reduce(into: []) { list, _ in
|
||||
list.append(.newMockProfile())
|
||||
}
|
||||
return ProfileManager(profiles: profiles)
|
||||
}()
|
||||
let tunnelEnvironment = InMemoryEnvironment()
|
||||
let tunnel = ExtendedTunnel(
|
||||
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: tunnelEnvironment)),
|
||||
@ -84,35 +83,35 @@ extension AppContext {
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
registry: registry,
|
||||
registry: Registry(),
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Shortcuts
|
||||
|
||||
extension IAPManager {
|
||||
public static var mock: IAPManager {
|
||||
AppContext.mock.iapManager
|
||||
public static var forPreviews: IAPManager {
|
||||
AppContext.forPreviews.iapManager
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileManager {
|
||||
public static var mock: ProfileManager {
|
||||
AppContext.mock.profileManager
|
||||
public static var forPreviews: ProfileManager {
|
||||
AppContext.forPreviews.profileManager
|
||||
}
|
||||
}
|
||||
|
||||
extension ExtendedTunnel {
|
||||
public static var mock: ExtendedTunnel {
|
||||
AppContext.mock.tunnel
|
||||
public static var forPreviews: ExtendedTunnel {
|
||||
AppContext.forPreviews.tunnel
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderManager {
|
||||
public static var mock: ProviderManager {
|
||||
AppContext.mock.providerManager
|
||||
public static var forPreviews: ProviderManager {
|
||||
AppContext.forPreviews.providerManager
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Profile+Mock.swift
|
||||
// Profile+Previews.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/4/24.
|
||||
@ -27,7 +27,7 @@ import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension Profile {
|
||||
public static let mock: Profile = {
|
||||
public static let forPreviews: Profile = {
|
||||
var profile = Profile.Builder()
|
||||
profile.name = "Mock profile"
|
||||
do {
|
||||
@ -63,7 +63,7 @@ extension Profile {
|
||||
|
||||
public static func newMockProfile(withName name: String? = nil) -> Profile {
|
||||
do {
|
||||
var copy = mock.builder(withNewId: true)
|
||||
var copy = forPreviews.builder(withNewId: true)
|
||||
copy.name = name ?? String(copy.id.uuidString.prefix(8))
|
||||
return try copy.tryBuild()
|
||||
} catch {
|
@ -76,9 +76,9 @@ private extension ConnectionStatusText {
|
||||
}
|
||||
|
||||
#Preview("Connected") {
|
||||
ConnectionStatusText(tunnel: .mock)
|
||||
ConnectionStatusText(tunnel: .forPreviews)
|
||||
.task {
|
||||
try? await ExtendedTunnel.mock.connect(with: .mock)
|
||||
try? await ExtendedTunnel.forPreviews.connect(with: .forPreviews)
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.withMockEnvironment()
|
||||
@ -95,9 +95,9 @@ private extension ConnectionStatusText {
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
return ConnectionStatusText(tunnel: .mock)
|
||||
return ConnectionStatusText(tunnel: .forPreviews)
|
||||
.task {
|
||||
try? await ExtendedTunnel.mock.connect(with: profile)
|
||||
try? await ExtendedTunnel.forPreviews.connect(with: profile)
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.withMockEnvironment()
|
||||
|
@ -30,14 +30,14 @@ import PassepartoutKit
|
||||
final class MockProfileProcessor: ProfileProcessor {
|
||||
var isIncludedCount = 0
|
||||
|
||||
var willRebuildCount = 0
|
||||
|
||||
var verifyCount = 0
|
||||
|
||||
var isIncludedBlock: (Profile) -> Bool = { _ in true }
|
||||
|
||||
var requiredFeaturesCount = 0
|
||||
|
||||
var requiredFeatures: Set<AppFeature>?
|
||||
|
||||
var willRebuildCount = 0
|
||||
|
||||
func title(for profile: Profile) -> String {
|
||||
profile.name
|
||||
}
|
||||
@ -51,13 +51,13 @@ final class MockProfileProcessor: ProfileProcessor {
|
||||
ProfilePreview(profile)
|
||||
}
|
||||
|
||||
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
|
||||
requiredFeaturesCount += 1
|
||||
return requiredFeatures
|
||||
}
|
||||
|
||||
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
|
||||
willRebuildCount += 1
|
||||
return builder
|
||||
}
|
||||
|
||||
func verify(_ profile: Profile) -> Set<AppFeature>? {
|
||||
verifyCount += 1
|
||||
return requiredFeatures
|
||||
}
|
||||
}
|
||||
|
@ -100,8 +100,8 @@ extension ProfileManagerTests {
|
||||
XCTAssertTrue(sut.isReady)
|
||||
|
||||
XCTAssertEqual(processor.isIncludedCount, 1)
|
||||
XCTAssertEqual(processor.requiredFeaturesCount, 1)
|
||||
XCTAssertEqual(processor.willRebuildCount, 0)
|
||||
XCTAssertEqual(processor.verifyCount, 1)
|
||||
XCTAssertEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,10 @@ import UITesting
|
||||
|
||||
extension AppContext {
|
||||
static let shared: AppContext = {
|
||||
let iapManager: IAPManager = .sharedForApp
|
||||
let processor = InAppProcessor.shared(iapManager) {
|
||||
$0.localizedPreview
|
||||
}
|
||||
|
||||
// MARK: ProfileManager
|
||||
|
||||
@ -65,7 +69,7 @@ extension AppContext {
|
||||
backupRepository: Configuration.ProfileManager.backupProfileRepository,
|
||||
remoteRepositoryBlock: remoteRepositoryBlock,
|
||||
mirrorsRemoteRepository: Configuration.ProfileManager.mirrorsRemoteRepository,
|
||||
processor: IAPManager.sharedProcessor
|
||||
processor: processor
|
||||
)
|
||||
}()
|
||||
|
||||
@ -74,7 +78,7 @@ extension AppContext {
|
||||
let tunnel = ExtendedTunnel(
|
||||
tunnel: Tunnel(strategy: Configuration.ExtendedTunnel.strategy),
|
||||
environment: .shared,
|
||||
processor: IAPManager.sharedProcessor,
|
||||
processor: processor,
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
|
||||
@ -118,7 +122,7 @@ extension AppContext {
|
||||
let migrationManager = MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation)
|
||||
|
||||
return AppContext(
|
||||
iapManager: .sharedForApp,
|
||||
iapManager: iapManager,
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
|
@ -37,44 +37,6 @@ extension IAPManager {
|
||||
betaChecker: Configuration.IAPManager.betaChecker,
|
||||
productsAtBuild: Configuration.IAPManager.productsAtBuild
|
||||
)
|
||||
|
||||
static let sharedProcessor = InAppProcessor(
|
||||
iapManager: sharedForApp,
|
||||
title: {
|
||||
Configuration.ProfileManager.sharedTitle($0)
|
||||
},
|
||||
isIncluded: {
|
||||
Configuration.ProfileManager.isIncluded($0, $1)
|
||||
},
|
||||
preview: {
|
||||
$0.localizedPreview
|
||||
},
|
||||
verify: { iap, profile in
|
||||
do {
|
||||
try iap.verify(profile)
|
||||
return nil
|
||||
} catch AppError.ineligibleProfile(let requiredFeatures) {
|
||||
return requiredFeatures
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
willRebuild: { _, builder in
|
||||
builder
|
||||
},
|
||||
willInstall: { iap, profile in
|
||||
try iap.verify(profile)
|
||||
|
||||
// validate provider modules
|
||||
do {
|
||||
_ = try profile.withProviderModules()
|
||||
return profile
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
@ -90,7 +52,7 @@ private extension Configuration.IAPManager {
|
||||
|
||||
@MainActor
|
||||
static let simulatedInAppHelper: any AppProductHelper = {
|
||||
guard !AppCommandLine.contains(.fakeIAP) else {
|
||||
if AppCommandLine.contains(.fakeIAP) {
|
||||
return FakeAppProductHelper()
|
||||
}
|
||||
return inAppHelper
|
||||
@ -98,7 +60,7 @@ private extension Configuration.IAPManager {
|
||||
|
||||
@MainActor
|
||||
static var simulatedAppReceiptReader: AppReceiptReader {
|
||||
guard !AppCommandLine.contains(.fakeIAP) else {
|
||||
if AppCommandLine.contains(.fakeIAP) {
|
||||
guard let mockHelper = inAppHelper as? FakeAppProductHelper else {
|
||||
fatalError("When .isFakeIAP, productHelper is expected to be MockAppProductHelper")
|
||||
}
|
||||
|
@ -89,6 +89,50 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: InAppProcessor
|
||||
|
||||
extension InAppProcessor {
|
||||
|
||||
@MainActor
|
||||
static func shared(_ iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
|
||||
InAppProcessor(
|
||||
iapManager: iapManager,
|
||||
title: {
|
||||
Configuration.ProfileManager.sharedTitle($0)
|
||||
},
|
||||
isIncluded: {
|
||||
Configuration.ProfileManager.isIncluded($0, $1)
|
||||
},
|
||||
preview: preview,
|
||||
requiredFeatures: { iap, profile in
|
||||
do {
|
||||
try iap.verify(profile)
|
||||
return nil
|
||||
} catch AppError.ineligibleProfile(let requiredFeatures) {
|
||||
return requiredFeatures
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
willRebuild: { _, builder in
|
||||
builder
|
||||
},
|
||||
willInstall: { iap, profile in
|
||||
try iap.verify(profile)
|
||||
|
||||
// validate provider modules
|
||||
do {
|
||||
_ = try profile.withProviderModules()
|
||||
return profile
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
enum Configuration {
|
||||
|
73
Passepartout/Shared/Testing/AppContext+Testing.swift
Normal file
73
Passepartout/Shared/Testing/AppContext+Testing.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// AppContext+Testing.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/28/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
import UILibrary
|
||||
|
||||
extension AppContext {
|
||||
static func forUITesting(withRegistry registry: Registry) -> AppContext {
|
||||
let iapManager = IAPManager(
|
||||
customUserLevel: .subscriber,
|
||||
inAppHelper: FakeAppProductHelper(),
|
||||
receiptReader: FakeAppReceiptReader(),
|
||||
betaChecker: TestFlightChecker(),
|
||||
productsAtBuild: { _ in
|
||||
[]
|
||||
}
|
||||
)
|
||||
let processor = InAppProcessor.shared(iapManager) {
|
||||
$0.localizedPreview
|
||||
}
|
||||
|
||||
let profileManager: ProfileManager = .forTesting(
|
||||
withRegistry: .shared,
|
||||
processor: processor
|
||||
)
|
||||
let tunnelEnvironment = InMemoryEnvironment()
|
||||
let tunnel = ExtendedTunnel(
|
||||
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: tunnelEnvironment)),
|
||||
environment: tunnelEnvironment,
|
||||
processor: processor,
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
let providerManager = ProviderManager(
|
||||
repository: InMemoryProviderRepository()
|
||||
)
|
||||
let migrationManager = MigrationManager()
|
||||
|
||||
return AppContext(
|
||||
iapManager: iapManager,
|
||||
migrationManager: migrationManager,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
registry: registry,
|
||||
tunnel: tunnel,
|
||||
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt
|
||||
)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// ProfileManager+Mock.swift
|
||||
// ProfileManager+Testing.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/28/24.
|
||||
@ -28,7 +28,7 @@ import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
extension ProfileManager {
|
||||
public static func mock(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager {
|
||||
public static func forTesting(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager {
|
||||
let repository = InMemoryProfileRepository()
|
||||
let remoteRepository = InMemoryProfileRepository()
|
||||
let manager = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
Loading…
Reference in New Issue
Block a user