diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index a5f75953..74a94723 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -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 = ""; }; 0E757F182CD0CFFD006E13E1 /* LoginItem.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoginItem.entitlements; sourceTree = ""; }; 0E757F212CD0D2B7006E13E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 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 = ""; }; 0E7D0EAD2CAEA47700A2F28D /* Passepartout.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.xctestplan; sourceTree = ""; }; 0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; @@ -128,6 +140,9 @@ 0E7E3D5F2B9345FD002BBDB4 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = ""; }; 0E7E3D662B9345FD002BBDB4 /* Tunnel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Tunnel.entitlements; sourceTree = ""; }; 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; + 0E7F46012CF7D5E300B1C53A /* AppEnvironment+IAP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvironment+IAP.swift"; sourceTree = ""; }; + 0E7F460B2CF7F01600B1C53A /* PassepartoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassepartoutUITests.swift; sourceTree = ""; }; + 0E7F460C2CF7F01600B1C53A /* XCUIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Extensions.swift"; sourceTree = ""; }; 0E8D852F2C328CA1005493DE /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = ""; }; 0EB08B972CA46F4900A02591 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppPlist.strings; sourceTree = ""; }; @@ -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 = ""; @@ -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 = ""; }; + 0E7F460D2CF7F01600B1C53A /* UITests */ = { + isa = PBXGroup; + children = ( + 0E7F460B2CF7F01600B1C53A /* PassepartoutUITests.swift */, + 0E7F460C2CF7F01600B1C53A /* XCUIApplication+Extensions.swift */, + ); + path = UITests; + sourceTree = ""; + }; 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; diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index 21b0fa82..94ef51de 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -45,6 +45,17 @@ ReferencedContainer = "container:Passepartout.xcodeproj"> + + + + + + + + + + @@ -106,16 +129,6 @@ value = "1" isEnabled = "NO"> - - - - diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/PassepartoutIntents.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/PassepartoutIntents.xcscheme index 8a40ef94..2874df24 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/PassepartoutIntents.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/PassepartoutIntents.xcscheme @@ -56,6 +56,17 @@ ReferencedContainer = "container:Passepartout.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift index 3826bed3..024ed440 100644 --- a/Passepartout/App/AppDelegate.swift +++ b/Passepartout/App/AppDelegate.swift @@ -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( diff --git a/Passepartout/App/Platforms/App+iOS.swift b/Passepartout/App/Platforms/App+iOS.swift index a3157312..bd2cbbb4 100644 --- a/Passepartout/App/Platforms/App+iOS.swift +++ b/Passepartout/App/Platforms/App+iOS.swift @@ -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)) } } } diff --git a/Passepartout/App/Platforms/App+macOS.swift b/Passepartout/App/Platforms/App+macOS.swift index 75307915..3efb7257 100644 --- a/Passepartout/App/Platforms/App+macOS.swift +++ b/Passepartout/App/Platforms/App+macOS.swift @@ -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) diff --git a/Passepartout/App/Platforms/App+tvOS.swift b/Passepartout/App/Platforms/App+tvOS.swift index 0c9066ca..ddf36e68 100644 --- a/Passepartout/App/Platforms/App+tvOS.swift +++ b/Passepartout/App/Platforms/App+tvOS.swift @@ -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)) } } } diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift index 8318db14..aa752307 100644 --- a/Passepartout/Library/Package.swift +++ b/Passepartout/Library/Package.swift @@ -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"] diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift index f4791308..b426a4ec 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift @@ -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 } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/OnboardingModifier.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/OnboardingModifier.swift index 0cc633fa..846fffb8 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/OnboardingModifier.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/OnboardingModifier.swift @@ -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() diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift index 43fb0eaf..5da05caf 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContextMenu.swift @@ -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 { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift index 7759ae84..1e32f507 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift @@ -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) } } diff --git a/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileListView.swift b/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileListView.swift index e06346a6..5b342abd 100644 --- a/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileListView.swift +++ b/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileListView.swift @@ -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 } diff --git a/Passepartout/Library/Sources/UILibrary/Extensions/EnvironmentValues+Extensions.swift b/Passepartout/Library/Sources/UILibrary/Extensions/EnvironmentValues+Extensions.swift new file mode 100644 index 00000000..ba80f88c --- /dev/null +++ b/Passepartout/Library/Sources/UILibrary/Extensions/EnvironmentValues+Extensions.swift @@ -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 . +// + +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 +} diff --git a/Passepartout/Library/Sources/UITesting/Domain/AccessibilityInfo.swift b/Passepartout/Library/Sources/UITesting/Domain/AccessibilityInfo.swift new file mode 100644 index 00000000..8ffdb6d7 --- /dev/null +++ b/Passepartout/Library/Sources/UITesting/Domain/AccessibilityInfo.swift @@ -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 . +// + +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 + } +} diff --git a/Passepartout/Library/Sources/UITesting/Domain/View+Accessibility.swift b/Passepartout/Library/Sources/UITesting/Domain/View+Accessibility.swift new file mode 100644 index 00000000..ba945809 --- /dev/null +++ b/Passepartout/Library/Sources/UITesting/Domain/View+Accessibility.swift @@ -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 . +// + +import SwiftUI + +extension View { + public func uiAccessibility(_ info: AccessibilityInfo) -> some View { + accessibilityIdentifier(info.id) + } +} diff --git a/Passepartout/Library/Sources/UITesting/IPC/AppCommandLine.swift b/Passepartout/Library/Sources/UITesting/IPC/AppCommandLine.swift new file mode 100644 index 00000000..32dd1d70 --- /dev/null +++ b/Passepartout/Library/Sources/UITesting/IPC/AppCommandLine.swift @@ -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 . +// + +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) + } +} diff --git a/Passepartout/Library/Sources/UITesting/IPC/AppEnvironment.swift b/Passepartout/Library/Sources/UITesting/IPC/AppEnvironment.swift new file mode 100644 index 00000000..7a71c83f --- /dev/null +++ b/Passepartout/Library/Sources/UITesting/IPC/AppEnvironment.swift @@ -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 . +// + +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] + } +} diff --git a/Passepartout/Library/Sources/UITesting/Views/App.swift b/Passepartout/Library/Sources/UITesting/Views/App.swift new file mode 100644 index 00000000..98d74464 --- /dev/null +++ b/Passepartout/Library/Sources/UITesting/Views/App.swift @@ -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 . +// + +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) + } +} diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift index 4b33bf3f..45f28d27 100644 --- a/Passepartout/Shared/AppContext+Shared.swift +++ b/Passepartout/Shared/AppContext+Shared.swift @@ -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, diff --git a/Passepartout/Shared/AppEnvironment+IAP.swift b/Passepartout/Shared/AppEnvironment+IAP.swift new file mode 100644 index 00000000..f7505b3d --- /dev/null +++ b/Passepartout/Shared/AppEnvironment+IAP.swift @@ -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 . +// + +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 + } +} diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift index 7f33770c..5b790670 100644 --- a/Passepartout/Shared/Shared+App.swift +++ b/Passepartout/Shared/Shared+App.swift @@ -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") } diff --git a/Passepartout/Shared/Shared.swift b/Passepartout/Shared/Shared.swift index 672f0ed9..b0487c6d 100644 --- a/Passepartout/Shared/Shared.swift +++ b/Passepartout/Shared/Shared.swift @@ -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: { diff --git a/Passepartout/UITests/PassepartoutUITests.swift b/Passepartout/UITests/PassepartoutUITests.swift new file mode 100644 index 00000000..cb5cda58 --- /dev/null +++ b/Passepartout/UITests/PassepartoutUITests.swift @@ -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 . +// + +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) + } +} diff --git a/Passepartout/UITests/XCUIApplication+Extensions.swift b/Passepartout/UITests/XCUIApplication+Extensions.swift new file mode 100644 index 00000000..c4a92d95 --- /dev/null +++ b/Passepartout/UITests/XCUIApplication+Extensions.swift @@ -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 . +// + +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] + } + } +}