Add target for UI tests (#959)

Create UITesting target with:

- AppCommandLine/AppEnvironment: strongly typed refactoring of PP_*
environment values
- AccessibilityInfo: identifies and locates elements for UI testing

Make the app behave differently when launched with `.uiTesting`, and
expose the flag to SwiftUI via `.environment(\.isUITesting)` to:

- Use the mock AppContext
- Skip onboarding

Add PassepartoutUITests target with two screenshot tests:

- Connected screen
- Profile modal
This commit is contained in:
Davide 2024-11-28 01:30:26 +01:00 committed by GitHub
parent fc711ae98f
commit 581673c617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 706 additions and 56 deletions

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
0E2D68DC2CF7CF7500DC95BC /* UITesting in Frameworks */ = {isa = PBXBuildFile; productRef = 0E2D68DB2CF7CF7500DC95BC /* UITesting */; };
0E3E22962CE53510005135DF /* AppUIMain in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, macos, ); productRef = 0E3E22952CE53510005135DF /* AppUIMain */; };
0E3E22982CE53510005135DF /* AppUITV in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, ); productRef = 0E3E22972CE53510005135DF /* AppUITV */; };
0E3FF4BA2CE3AFBC00BFF640 /* Profiles.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 0E3FF4B72CE3AFBC00BFF640 /* Profiles.sqlite */; };
@ -20,6 +21,9 @@
0E7C3CCD2C9AF44600B72E69 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */; };
0E7E3D692B9345FD002BBDB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E7E3D5C2B9345FD002BBDB4 /* Assets.xcassets */; };
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D5F2B9345FD002BBDB4 /* PassepartoutApp.swift */; };
0E7F46022CF7D5E300B1C53A /* AppEnvironment+IAP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7F46012CF7D5E300B1C53A /* AppEnvironment+IAP.swift */; };
0E7F460E2CF7F01600B1C53A /* PassepartoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7F460B2CF7F01600B1C53A /* PassepartoutUITests.swift */; };
0E7F460F2CF7F01600B1C53A /* XCUIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7F460C2CF7F01600B1C53A /* XCUIApplication+Extensions.swift */; };
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; };
0EB08B982CA46F4900A02591 /* AppPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0EB08B962CA46F4900A02591 /* AppPlist.strings */; };
0EBE80DC2BF55C0E00E36A20 /* TunnelLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0EBE80DB2BF55C0E00E36A20 /* TunnelLibrary */; };
@ -57,6 +61,13 @@
remoteGlobalIDString = 0E757F0F2CD0CFFC006E13E1;
remoteInfo = PassepartoutLoginItem;
};
0E78FE522CF799F400B0C5BF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0E06D1872B87629100176E1D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0E06D18E2B87629100176E1D;
remoteInfo = Passepartout;
};
0EC332D02B8A1808000B9C2F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0E06D1872B87629100176E1D /* Project object */;
@ -121,6 +132,7 @@
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>"; };
0E757F212CD0D2B7006E13E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
0E78FE4C2CF799F400B0C5BF /* PassepartoutUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
0E7D0EAD2CAEA47700A2F28D /* Passepartout.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.xctestplan; sourceTree = "<group>"; };
0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
@ -128,6 +140,9 @@
0E7E3D5F2B9345FD002BBDB4 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = "<group>"; };
0E7E3D662B9345FD002BBDB4 /* Tunnel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Tunnel.entitlements; sourceTree = "<group>"; };
0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
0E7F46012CF7D5E300B1C53A /* AppEnvironment+IAP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvironment+IAP.swift"; sourceTree = "<group>"; };
0E7F460B2CF7F01600B1C53A /* PassepartoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassepartoutUITests.swift; sourceTree = "<group>"; };
0E7F460C2CF7F01600B1C53A /* XCUIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Extensions.swift"; sourceTree = "<group>"; };
0E8D852F2C328CA1005493DE /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = "<group>"; };
0EB08B972CA46F4900A02591 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppPlist.strings; sourceTree = "<group>"; };
@ -165,6 +180,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
0E78FE492CF799F400B0C5BF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0E2D68DC2CF7CF7500DC95BC /* UITesting in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0EC332C52B8A1808000B9C2F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -213,6 +236,7 @@
0EDE56F02CABE42E0082D21C /* PassepartoutIntents.appex */,
0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */,
0E3FF4AE2CE3AF6F00BFF640 /* PassepartoutTests.xctest */,
0E78FE4C2CF799F400B0C5BF /* PassepartoutUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -265,6 +289,7 @@
0E7E3D612B9345FD002BBDB4 /* Shared */,
0E3FF4A92CE3AF4700BFF640 /* Tests */,
0E7E3D652B9345FD002BBDB4 /* Tunnel */,
0E7F460D2CF7F01600B1C53A /* UITests */,
0EBE80DD2BF55C9100E36A20 /* Library */,
);
path = Passepartout;
@ -289,6 +314,7 @@
isa = PBXGroup;
children = (
0EC797402B9378E000C093B7 /* AppContext+Shared.swift */,
0E7F46012CF7D5E300B1C53A /* AppEnvironment+IAP.swift */,
0EC797412B9378E000C093B7 /* Shared.swift */,
0E483E822CE6501100584B32 /* Shared+App.swift */,
0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */,
@ -306,6 +332,15 @@
path = Tunnel;
sourceTree = "<group>";
};
0E7F460D2CF7F01600B1C53A /* UITests */ = {
isa = PBXGroup;
children = (
0E7F460B2CF7F01600B1C53A /* PassepartoutUITests.swift */,
0E7F460C2CF7F01600B1C53A /* XCUIApplication+Extensions.swift */,
);
path = UITests;
sourceTree = "<group>";
};
0ED61CF62CD04174008FE259 /* Platforms */ = {
isa = PBXGroup;
children = (
@ -395,6 +430,27 @@
productReference = 0E757F102CD0CFFC006E13E1 /* PassepartoutLoginItem.app */;
productType = "com.apple.product-type.application";
};
0E78FE4B2CF799F400B0C5BF /* PassepartoutUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0E78FE562CF799F400B0C5BF /* Build configuration list for PBXNativeTarget "PassepartoutUITests" */;
buildPhases = (
0E78FE482CF799F400B0C5BF /* Sources */,
0E78FE492CF799F400B0C5BF /* Frameworks */,
0E78FE4A2CF799F400B0C5BF /* Resources */,
);
buildRules = (
);
dependencies = (
0E78FE532CF799F400B0C5BF /* PBXTargetDependency */,
);
name = PassepartoutUITests;
packageProductDependencies = (
0E2D68DB2CF7CF7500DC95BC /* UITesting */,
);
productName = PassepartoutUITests;
productReference = 0E78FE4C2CF799F400B0C5BF /* PassepartoutUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
0EC332C72B8A1808000B9C2F /* PassepartoutTunnel */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0EC332D32B8A1808000B9C2F /* Build configuration list for PBXNativeTarget "PassepartoutTunnel" */;
@ -441,7 +497,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1540;
LastSwiftUpdateCheck = 1610;
LastUpgradeCheck = 1610;
TargetAttributes = {
0E06D18E2B87629100176E1D = {
@ -453,6 +509,10 @@
0E757F0F2CD0CFFC006E13E1 = {
CreatedOnToolsVersion = 15.4;
};
0E78FE4B2CF799F400B0C5BF = {
CreatedOnToolsVersion = 16.1;
TestTargetID = 0E06D18E2B87629100176E1D;
};
0EC332C72B8A1808000B9C2F = {
CreatedOnToolsVersion = 15.2;
};
@ -479,6 +539,7 @@
0E757F0F2CD0CFFC006E13E1 /* PassepartoutLoginItem */,
0E3FF4AD2CE3AF6F00BFF640 /* PassepartoutTests */,
0EC332C72B8A1808000B9C2F /* PassepartoutTunnel */,
0E78FE4B2CF799F400B0C5BF /* PassepartoutUITests */,
);
};
/* End PBXProject section */
@ -510,6 +571,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
0E78FE4A2CF799F400B0C5BF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
0EC332C62B8A1808000B9C2F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -560,6 +628,7 @@
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
0E483E842CE6501100584B32 /* Shared+App.swift in Sources */,
0EC797432B9378E000C093B7 /* Shared.swift in Sources */,
0E7F46022CF7D5E300B1C53A /* AppEnvironment+IAP.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -581,6 +650,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
0E78FE482CF799F400B0C5BF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0E7F460E2CF7F01600B1C53A /* PassepartoutUITests.swift in Sources */,
0E7F460F2CF7F01600B1C53A /* XCUIApplication+Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0EC332C42B8A1808000B9C2F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -623,6 +701,11 @@
target = 0E757F0F2CD0CFFC006E13E1 /* PassepartoutLoginItem */;
targetProxy = 0E757F242CD0D812006E13E1 /* PBXContainerItemProxy */;
};
0E78FE532CF799F400B0C5BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0E06D18E2B87629100176E1D /* Passepartout */;
targetProxy = 0E78FE522CF799F400B0C5BF /* PBXContainerItemProxy */;
};
0EC332D12B8A1808000B9C2F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0EC332C72B8A1808000B9C2F /* PassepartoutTunnel */;
@ -962,6 +1045,36 @@
};
name = Release;
};
0E78FE542CF799F400B0C5BF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.algoritmico.ios.PassepartoutUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = Passepartout;
};
name = Debug;
};
0E78FE552CF799F400B0C5BF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.algoritmico.ios.PassepartoutUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = Passepartout;
};
name = Release;
};
0EC332D42B8A1808000B9C2F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1108,6 +1221,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0E78FE562CF799F400B0C5BF /* Build configuration list for PBXNativeTarget "PassepartoutUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0E78FE542CF799F400B0C5BF /* Debug */,
0E78FE552CF799F400B0C5BF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0EC332D32B8A1808000B9C2F /* Build configuration list for PBXNativeTarget "PassepartoutTunnel" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1129,6 +1251,10 @@
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
0E2D68DB2CF7CF7500DC95BC /* UITesting */ = {
isa = XCSwiftPackageProductDependency;
productName = UITesting;
};
0E3E22952CE53510005135DF /* AppUIMain */ = {
isa = XCSwiftPackageProductDependency;
productName = AppUIMain;

View File

@ -45,6 +45,17 @@
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0E78FE4B2CF799F400B0C5BF"
BuildableName = "PassepartoutUITests.xctest"
BlueprintName = "PassepartoutUITests"
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
@ -68,6 +79,18 @@
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-pp_fake_iap"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-pp_ui_tests"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-pp_fake_migration"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
@ -106,16 +129,6 @@
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PP_FAKE_IAP"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PP_FAKE_MIGRATION"
value = "1"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<StoreKitConfigurationFileReference
identifier = "../../Passepartout/Passepartout.storekit">

View File

@ -56,6 +56,17 @@
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0E78FE4B2CF799F400B0C5BF"
BuildableName = "PassepartoutUITests.xctest"
BlueprintName = "PassepartoutUITests"
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -55,6 +55,17 @@
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0E78FE4B2CF799F400B0C5BF"
BuildableName = "PassepartoutUITests.xctest"
BlueprintName = "PassepartoutUITests"
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0E78FE4B2CF799F400B0C5BF"
BuildableName = "PassepartoutUITests.xctest"
BlueprintName = "PassepartoutUITests"
ReferencedContainer = "container:Passepartout.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -27,11 +27,16 @@ import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
import UITesting
@MainActor
final class AppDelegate: NSObject {
let context: AppContext = .shared
// let context: AppContext = .mock(withRegistry: .shared)
let context: AppContext = {
guard !AppCommandLine.contains(.uiTesting) else {
return .mock(withRegistry: .shared)
}
return .shared
}()
#if os(macOS)
let settings = MacSettingsModel(

View File

@ -27,6 +27,7 @@
import AppUIMain
import SwiftUI
import UITesting
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
@ -49,6 +50,7 @@ extension PassepartoutApp {
}
.themeLockScreen()
.withEnvironment(from: context, theme: theme)
.environment(\.isUITesting, AppCommandLine.contains(.uiTesting))
}
}
}

View File

@ -31,6 +31,7 @@ import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
import UITesting
extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
@ -57,10 +58,11 @@ extension PassepartoutApp {
var body: some Scene {
Window(appName, id: appName) {
contentView()
.withEnvironment(from: context, theme: theme)
.onReceive(didActivateNotificationPublisher) {
context.onApplicationActive()
}
.withEnvironment(from: context, theme: theme)
.environment(\.isUITesting, AppCommandLine.contains(.uiTesting))
}
.defaultSize(width: 600, height: 400)
@ -69,6 +71,7 @@ extension PassepartoutApp {
.frame(minWidth: 300, minHeight: 300)
.withEnvironment(from: context, theme: theme)
.environmentObject(settings)
.environment(\.isUITesting, AppCommandLine.contains(.uiTesting))
}
MenuBarExtra {
AppMenu(
@ -77,6 +80,7 @@ extension PassepartoutApp {
)
.withEnvironment(from: context, theme: theme)
.environmentObject(settings)
.environment(\.isUITesting, AppCommandLine.contains(.uiTesting))
} label: {
AppMenuImage(tunnel: context.tunnel)
.environmentObject(theme)

View File

@ -27,6 +27,7 @@
import AppUITV
import SwiftUI
import UITesting
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
@ -45,6 +46,7 @@ extension PassepartoutApp {
}
}
.withEnvironment(from: context, theme: theme)
.environment(\.isUITesting, AppCommandLine.contains(.uiTesting))
}
}
}

View File

@ -44,6 +44,10 @@ let package = Package(
.library(
name: "UILibrary",
targets: ["UILibrary"]
),
.library(
name: "UITesting",
targets: ["UITesting"]
)
],
dependencies: [
@ -165,12 +169,16 @@ let package = Package(
"AppDataProfiles",
"AppDataProviders",
"CommonAPI",
"CommonLibrary"
"CommonLibrary",
"UITesting"
],
resources: [
.process("Resources")
]
),
.target(
name: "UITesting"
),
.testTarget(
name: "AppUIMainTests",
dependencies: ["AppUIMain"]

View File

@ -27,6 +27,7 @@ import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
import UITesting
struct InstalledProfileView: View, Routable {
@ -54,6 +55,7 @@ struct InstalledProfileView: View, Routable {
debugChanges()
return HStack(alignment: .center) {
cardView
.uiAccessibility(.App.installedProfile)
Spacer()
toggleButton
}

View File

@ -29,6 +29,9 @@ import SwiftUI
struct OnboardingModifier: ViewModifier {
@Environment(\.isUITesting)
private var isUITesting
@AppStorage(UIPreference.onboardingStep.key)
private var step: OnboardingStep?
@ -67,6 +70,10 @@ struct OnboardingModifier: ViewModifier {
private extension OnboardingModifier {
func advance() {
guard !isUITesting else {
pp_log(.app, .info, "UI tests, skip onboarding")
return
}
Task {
try await Task.sleep(for: .milliseconds(300))
doAdvance()

View File

@ -27,6 +27,7 @@ import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
import UITesting
struct ProfileContextMenu: View, Routable {
enum Style {
@ -128,6 +129,7 @@ private extension ProfileContextMenu {
} label: {
ThemeImageLabel(Strings.Global.Actions.edit, .profileEdit)
}
.uiAccessibility(.ProfileMenu.edit)
}
var profileDuplicateButton: some View {

View File

@ -165,6 +165,7 @@ private extension ProfileRowView {
}
)
.foregroundStyle(.primary)
.uiAccessibility(.App.profileToggle)
}
var attributesView: some View {
@ -197,6 +198,7 @@ private extension ProfileRowView {
.foregroundStyle(.secondary)
.buttonStyle(.plain)
#endif
.uiAccessibility(.App.profileMenu)
}
}

View File

@ -52,7 +52,7 @@ struct ProfileListView: View {
headerView
.frame(maxWidth: .infinity, alignment: .leading)
List {
ForEach(previews, id: \.id, content: toggleButton(for:))
ForEach(allPreviews, id: \.id, content: toggleButton(for:))
}
.themeList()
.themeProgress(if: false, isEmpty: !profileManager.hasProfiles) {
@ -64,7 +64,7 @@ struct ProfileListView: View {
}
private extension ProfileListView {
var previews: [ProfilePreview] {
var allPreviews: [ProfilePreview] {
profileManager.previews
}

View File

@ -0,0 +1,41 @@
//
// EnvironmentValues+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 SwiftUI
extension EnvironmentValues {
public var isUITesting: Bool {
get {
self[IsUITestingKey.self]
}
set {
self[IsUITestingKey.self] = newValue
}
}
}
private struct IsUITestingKey: EnvironmentKey {
static let defaultValue = false
}

View File

@ -0,0 +1,51 @@
//
// AccessibilityInfo.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 Foundation
public struct AccessibilityInfo: Equatable {
public enum ElementType {
case button
case menu
case menuItem
case text
}
public let id: String
public let elementType: ElementType
public init(_ id: String, _ elementType: ElementType) {
self.id = id
self.elementType = elementType
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}

View File

@ -0,0 +1,32 @@
//
// View+Accessibility.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 SwiftUI
extension View {
public func uiAccessibility(_ info: AccessibilityInfo) -> some View {
accessibilityIdentifier(info.id)
}
}

View File

@ -0,0 +1,40 @@
//
// AppCommandLine.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 Foundation
public enum AppCommandLine {
public enum Value: String {
case fakeIAP = "-pp_fake_iap"
case fakeMigration = "-pp_fake_migration"
case uiTesting = "-pp_ui_tests"
}
public static func contains(_ argument: Value) -> Bool {
CommandLine.arguments.contains(argument.rawValue)
}
}

View File

@ -0,0 +1,36 @@
//
// AppEnvironment.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 Foundation
public enum AppEnvironment {
public enum Key: String {
case userLevel = "PP_USER_LEVEL"
}
public static func string(for key: Key) -> String? {
ProcessInfo.processInfo.environment[key.rawValue]
}
}

View File

@ -0,0 +1,40 @@
//
// App.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 Foundation
extension AccessibilityInfo {
public enum App {
public static let installedProfile = AccessibilityInfo("app.installedProfile", .text)
public static let profileToggle = AccessibilityInfo("app.profileToggle", .button)
public static let profileMenu = AccessibilityInfo("app.profileMenu", .menu)
}
public enum ProfileMenu {
public static let edit = AccessibilityInfo("app.profileMenu.edit", .menuItem)
}
}

View File

@ -32,6 +32,7 @@ import Foundation
import LegacyV2
import PassepartoutKit
import UILibrary
import UITesting
// shared registry and environment are picked from Shared.swift
@ -105,7 +106,7 @@ extension AppContext {
)
)
let migrationSimulation: MigrationManager.Simulation?
if Configuration.Environment.isFakeMigration {
if AppCommandLine.contains(.fakeMigration) {
migrationSimulation = MigrationManager.Simulation(
fakeProfiles: true,
maxMigrationTime: 3.0,

View File

@ -0,0 +1,46 @@
//
// AppEnvironment+IAP.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 CommonIAP
import Foundation
import PassepartoutKit
import UITesting
extension AppEnvironment {
public static var userLevel: AppUserLevel? {
if let envString = string(for: .userLevel),
let envValue = Int(envString),
let testAppType = AppUserLevel(rawValue: envValue) {
return testAppType
}
if let infoValue = BundleConfiguration.mainIntegerIfPresent(for: .userLevel),
let testAppType = AppUserLevel(rawValue: infoValue) {
return testAppType
}
return nil
}
}

View File

@ -27,12 +27,13 @@ import CommonLibrary
import CommonUtils
import Foundation
import PassepartoutKit
import UITesting
extension IAPManager {
static let sharedForApp = IAPManager(
customUserLevel: Configuration.Environment.userLevel,
inAppHelper: Configuration.IAPManager.inAppHelper,
receiptReader: Configuration.IAPManager.appReceiptReader,
customUserLevel: AppEnvironment.userLevel,
inAppHelper: Configuration.IAPManager.simulatedInAppHelper,
receiptReader: Configuration.IAPManager.simulatedAppReceiptReader,
betaChecker: Configuration.IAPManager.betaChecker,
productsAtBuild: Configuration.IAPManager.productsAtBuild
)
@ -82,28 +83,19 @@ extension IAPManager {
// MARK: - Configuration
private extension Configuration.Environment {
static var userLevel: AppUserLevel? {
if let envString = ProcessInfo.processInfo.environment["PP_USER_LEVEL"],
let envValue = Int(envString),
let testAppType = AppUserLevel(rawValue: envValue) {
return testAppType
}
if let infoValue = BundleConfiguration.mainIntegerIfPresent(for: .userLevel),
let testAppType = AppUserLevel(rawValue: infoValue) {
return testAppType
}
return nil
}
}
private extension Configuration.IAPManager {
@MainActor
static var appReceiptReader: AppReceiptReader {
guard !Configuration.Environment.isFakeIAP else {
static let simulatedInAppHelper: any AppProductHelper = {
guard !AppCommandLine.contains(.fakeIAP) else {
return FakeAppProductHelper()
}
return inAppHelper
}()
@MainActor
static var simulatedAppReceiptReader: AppReceiptReader {
guard !AppCommandLine.contains(.fakeIAP) else {
guard let mockHelper = inAppHelper as? FakeAppProductHelper else {
fatalError("When .isFakeIAP, productHelper is expected to be MockAppProductHelper")
}

View File

@ -92,9 +92,6 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
// MARK: - Configuration
enum Configuration {
enum Environment {
}
enum ProfileManager {
}
@ -102,16 +99,6 @@ enum Configuration {
}
}
extension Configuration.Environment {
static var isFakeIAP: Bool {
ProcessInfo.processInfo.environment["PP_FAKE_IAP"] == "1"
}
static var isFakeMigration: Bool {
ProcessInfo.processInfo.environment["PP_FAKE_MIGRATION"] == "1"
}
}
// MARK: ProfileManager
extension Configuration.ProfileManager {
@ -140,9 +127,6 @@ extension Configuration.IAPManager {
@MainActor
static let inAppHelper: any AppProductHelper = {
guard !Configuration.Environment.isFakeIAP else {
return FakeAppProductHelper()
}
return StoreKitHelper(
products: AppProduct.all,
inAppIdentifier: {

View File

@ -0,0 +1,83 @@
//
// PassepartoutUITests.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 UITesting
import XCTest
@MainActor
final class PassepartoutUITests: XCTestCase {
private var app: XCUIApplication!
override func setUp() async throws {
continueAfterFailure = false
app = XCUIApplication()
app.appArguments = [.uiTesting]
app.launch()
}
func testConnected() async throws {
let container = app.get(.App.installedProfile)
XCTAssertTrue(container.waitForExistence(timeout: 1.0))
let profileToggle = app.get(.App.profileToggle).firstMatch
XCTAssertTrue(profileToggle.waitForExistence(timeout: 1.0))
profileToggle.tap()
try await Task.sleep(for: .seconds(3))
snapshot("1_Connected")
}
func testProfile() async throws {
let container = app.get(.App.installedProfile)
XCTAssertTrue(container.waitForExistence(timeout: 1.0))
let profileMenu = app.get(.App.profileMenu).firstMatch
XCTAssertTrue(profileMenu.waitForExistence(timeout: 1.0))
profileMenu.tap()
let editButton = app.get(.ProfileMenu.edit)
XCTAssertTrue(editButton.waitForExistence(timeout: 1.0))
editButton.tap()
try await Task.sleep(for: .seconds(2))
snapshot("2_Profile")
}
}
private extension PassepartoutUITests {
var window: XCUIElement {
app.windows.firstMatch
}
func snapshot(_ name: String) {
// let attachment = XCTAttachment(screenshot: window.screenshot())
// attachment.name = name
// attachment.lifetime = .keepAlways
// add(attachment)
}
}

View File

@ -0,0 +1,54 @@
//
// XCUIApplication+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 Foundation
import UITesting
import XCTest
extension XCUIApplication {
var appArguments: [AppCommandLine.Value] {
get {
launchArguments.compactMap(AppCommandLine.Value.init(rawValue:))
}
set {
launchArguments = newValue.map(\.rawValue)
}
}
}
extension XCUIElement {
func get(_ info: AccessibilityInfo) -> XCUIElement {
switch info.elementType {
case .button:
return buttons[info.id]
case .menu:
return menuButtons[info.id]
case .menuItem:
return menuItems[info.id]
case .text:
return staticTexts[info.id]
}
}
}