diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 846c366..41c4659 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,67 @@ jobs: - name: Build & Test macOS run: bash scripts/build-macos.sh + test-app: + name: Test App (macOS) + runs-on: self-hosted + steps: + - uses: actions/checkout@v6 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Write .env file + env: + ENV_B64: ${{ secrets.ENV_B64 }} + run: echo "$ENV_B64" | base64 --decode > test/testUITests/.env + - name: Run TestPlan + run: | + xcodebuild test \ + -project test/test.xcodeproj \ + -scheme test \ + -testPlan TestPlan \ + -destination 'platform=macOS' \ + CODE_SIGN_IDENTITY=- \ + DEVELOPMENT_TEAM="" + + test-app-ios: + name: Test App (iOS) + runs-on: self-hosted + steps: + - uses: actions/checkout@v6 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Write .env file + env: + ENV_B64: ${{ secrets.ENV_B64 }} + run: echo "$ENV_B64" | base64 --decode > test/testUITests/.env + - name: Find iOS Simulator + id: sim + run: | + xcrun simctl list runtimes + xcrun simctl list devices available + SIM_ID=$(xcrun simctl list devices available -j | python3 -c " + import json, sys + data = json.load(sys.stdin) + for runtime, devices in data['devices'].items(): + if 'iOS' in runtime: + for d in devices: + if d['isAvailable']: + print(d['udid']) + sys.exit(0) + sys.exit(1) + ") + echo "sim_id=$SIM_ID" >> "$GITHUB_OUTPUT" + - name: Run TestPlan + run: | + xcodebuild test \ + -project test/test.xcodeproj \ + -scheme test \ + -testPlan TestPlan \ + -destination 'platform=iOS Simulator,id=${{ steps.sim.outputs.sim_id }}' \ + CODE_SIGN_IDENTITY=- \ + DEVELOPMENT_TEAM="" + build-ios: name: Build (iOS) runs-on: macos-latest diff --git a/.gitignore b/.gitignore index d811c90..7b55a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,27 @@ .DS_Store /.build /Packages +.netrc +.env + +# Xcode xcuserdata/ DerivedData/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +# Swift Package Manager .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc -.env \ No newline at end of file +.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist \ No newline at end of file diff --git a/Sources/RxAuthSwift/InMemoryTokenStorage.swift b/Sources/RxAuthSwift/InMemoryTokenStorage.swift new file mode 100644 index 0000000..65bf3d3 --- /dev/null +++ b/Sources/RxAuthSwift/InMemoryTokenStorage.swift @@ -0,0 +1,73 @@ +import Foundation + +public final class InMemoryTokenStorage: TokenStorageProtocol, @unchecked Sendable { + private let lock = NSLock() + private var accessToken: String? + private var refreshToken: String? + private var expiresAt: Date? + + public init() {} + + public func saveAccessToken(_ token: String) throws { + lock.lock() + defer { lock.unlock() } + accessToken = token + } + + public func getAccessToken() -> String? { + lock.lock() + defer { lock.unlock() } + return accessToken + } + + public func deleteAccessToken() throws { + lock.lock() + defer { lock.unlock() } + accessToken = nil + } + + public func saveRefreshToken(_ token: String) throws { + lock.lock() + defer { lock.unlock() } + refreshToken = token + } + + public func getRefreshToken() -> String? { + lock.lock() + defer { lock.unlock() } + return refreshToken + } + + public func deleteRefreshToken() throws { + lock.lock() + defer { lock.unlock() } + refreshToken = nil + } + + public func saveExpiresAt(_ date: Date) throws { + lock.lock() + defer { lock.unlock() } + expiresAt = date + } + + public func getExpiresAt() -> Date? { + lock.lock() + defer { lock.unlock() } + return expiresAt + } + + public func isTokenExpired() -> Bool { + lock.lock() + defer { lock.unlock() } + guard let expiresAt else { return true } + return expiresAt.timeIntervalSinceNow < 600 + } + + public func clearAll() throws { + lock.lock() + defer { lock.unlock() } + accessToken = nil + refreshToken = nil + expiresAt = nil + } +} diff --git a/Sources/RxAuthSwift/Platform/MacOSWebAuthSession.swift b/Sources/RxAuthSwift/Platform/MacOSWebAuthSession.swift index 3f6b5c8..45f4ede 100644 --- a/Sources/RxAuthSwift/Platform/MacOSWebAuthSession.swift +++ b/Sources/RxAuthSwift/Platform/MacOSWebAuthSession.swift @@ -6,6 +6,7 @@ import WebKit final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider { private var window: NSWindow? private var webView: WKWebView? + private var urlTextField: NSTextField? private var continuation: CheckedContinuation? private var callbackScheme: String = "" @@ -25,6 +26,28 @@ final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider { webView.navigationDelegate = self self.webView = webView + let urlField = NSTextField() + urlField.isEditable = false + urlField.isSelectable = true + urlField.isBordered = true + urlField.bezelStyle = .roundedBezel + urlField.font = .systemFont(ofSize: 13) + urlField.lineBreakMode = .byTruncatingTail + urlField.cell?.truncatesLastVisibleLine = true + urlField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + urlField.setContentHuggingPriority(.defaultLow, for: .horizontal) + urlField.stringValue = url.absoluteString + self.urlTextField = urlField + + let toolbar = NSStackView(views: [urlField]) + toolbar.orientation = .horizontal + toolbar.edgeInsets = NSEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + toolbar.setHuggingPriority(.required, for: .horizontal) + + let container = NSStackView(views: [toolbar, webView]) + container.orientation = .vertical + container.spacing = 0 + let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 700), styleMask: [.titled, .closable, .resizable], @@ -32,7 +55,7 @@ final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider { defer: false ) window.title = "Sign In" - window.contentView = webView + window.contentView = container window.center() window.delegate = self window.makeKeyAndOrderFront(nil) @@ -42,10 +65,12 @@ final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider { } private func complete(with result: Result) { - window?.close() - window = nil webView?.navigationDelegate = nil webView = nil + urlTextField = nil + + window?.orderOut(nil) + window = nil let cont = continuation continuation = nil @@ -77,6 +102,12 @@ extension MacOSWebAuthSession: WKNavigationDelegate { } } + nonisolated func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + MainActor.assumeIsolated { + self.urlTextField?.stringValue = webView.url?.absoluteString ?? "" + } + } + nonisolated func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) { MainActor.assumeIsolated { self.complete(with: .failure(OAuthError.authenticationFailed(error.localizedDescription))) diff --git a/Sources/RxAuthSwiftUI/Components/PrimaryAuthButton.swift b/Sources/RxAuthSwiftUI/Components/PrimaryAuthButton.swift index a688677..071460a 100644 --- a/Sources/RxAuthSwiftUI/Components/PrimaryAuthButton.swift +++ b/Sources/RxAuthSwiftUI/Components/PrimaryAuthButton.swift @@ -43,7 +43,7 @@ public struct PrimaryAuthButton: View { .rxGlassProminentButton() .tint(accentColor) .disabled(isLoading) - .accessibilityIdentifier("primaryAuthButton") + .accessibilityIdentifier("sign-in-button") .accessibilityHint(isLoading ? "Authentication in progress" : "Double tap to sign in") } } diff --git a/Sources/RxAuthSwiftUI/RxSignInView.swift b/Sources/RxAuthSwiftUI/RxSignInView.swift index e124c13..9244d23 100644 --- a/Sources/RxAuthSwiftUI/RxSignInView.swift +++ b/Sources/RxAuthSwiftUI/RxSignInView.swift @@ -1,5 +1,5 @@ -import SwiftUI import RxAuthSwift +import SwiftUI public struct RxSignInView: View { @Bindable private var manager: OAuthManager diff --git a/test/TestPlan.xctestplan b/test/TestPlan.xctestplan new file mode 100644 index 0000000..b1920e7 --- /dev/null +++ b/test/TestPlan.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "D90F25C0-41BD-4163-83E8-C1BF58CB5D3F", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:test.xcodeproj", + "identifier" : "DF8439562F3C847600461950", + "name" : "testUITests" + } + } + ], + "version" : 1 +} diff --git a/test/test.xcodeproj/project.pbxproj b/test/test.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e752fb7 --- /dev/null +++ b/test/test.xcodeproj/project.pbxproj @@ -0,0 +1,524 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + DF84396C2F3C848E00461950 /* RxAuthSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DF84396B2F3C848E00461950 /* RxAuthSwift */; }; + DF84396E2F3C848E00461950 /* RxAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = DF84396D2F3C848E00461950 /* RxAuthSwiftUI */; }; + DF843A542F3C8B4400461950 /* TestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = DF843A532F3C8B4400461950 /* TestPlan.xctestplan */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DF8439582F3C847600461950 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DF8439362F3C847500461950 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DF84393D2F3C847500461950; + remoteInfo = test; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + DF84393E2F3C847500461950 /* test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = test.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DF8439572F3C847600461950 /* testUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = testUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DF843A532F3C8B4400461950 /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + DF8439402F3C847500461950 /* test */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = test; + sourceTree = ""; + }; + DF84395A2F3C847600461950 /* testUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = testUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + DF84393B2F3C847500461950 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DF84396E2F3C848E00461950 /* RxAuthSwiftUI in Frameworks */, + DF84396C2F3C848E00461950 /* RxAuthSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DF8439542F3C847600461950 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DF8439352F3C847500461950 = { + isa = PBXGroup; + children = ( + DF843A532F3C8B4400461950 /* TestPlan.xctestplan */, + DF8439402F3C847500461950 /* test */, + DF84395A2F3C847600461950 /* testUITests */, + DF84393F2F3C847500461950 /* Products */, + ); + sourceTree = ""; + }; + DF84393F2F3C847500461950 /* Products */ = { + isa = PBXGroup; + children = ( + DF84393E2F3C847500461950 /* test.app */, + DF8439572F3C847600461950 /* testUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DF84393D2F3C847500461950 /* test */ = { + isa = PBXNativeTarget; + buildConfigurationList = DF8439612F3C847600461950 /* Build configuration list for PBXNativeTarget "test" */; + buildPhases = ( + DF84393A2F3C847500461950 /* Sources */, + DF84393B2F3C847500461950 /* Frameworks */, + DF84393C2F3C847500461950 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + DF8439402F3C847500461950 /* test */, + ); + name = test; + packageProductDependencies = ( + DF84396B2F3C848E00461950 /* RxAuthSwift */, + DF84396D2F3C848E00461950 /* RxAuthSwiftUI */, + ); + productName = test; + productReference = DF84393E2F3C847500461950 /* test.app */; + productType = "com.apple.product-type.application"; + }; + DF8439562F3C847600461950 /* testUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DF8439672F3C847600461950 /* Build configuration list for PBXNativeTarget "testUITests" */; + buildPhases = ( + DF8439532F3C847600461950 /* Sources */, + DF8439542F3C847600461950 /* Frameworks */, + DF8439552F3C847600461950 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DF8439592F3C847600461950 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DF84395A2F3C847600461950 /* testUITests */, + ); + name = testUITests; + packageProductDependencies = ( + ); + productName = testUITests; + productReference = DF8439572F3C847600461950 /* testUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DF8439362F3C847500461950 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + DF84393D2F3C847500461950 = { + CreatedOnToolsVersion = 26.3; + }; + DF8439562F3C847600461950 = { + CreatedOnToolsVersion = 26.3; + TestTargetID = DF84393D2F3C847500461950; + }; + }; + }; + buildConfigurationList = DF8439392F3C847500461950 /* Build configuration list for PBXProject "test" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DF8439352F3C847500461950; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + DF84396A2F3C848E00461950 /* XCLocalSwiftPackageReference "../../RxAuthSwift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = DF84393F2F3C847500461950 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DF84393D2F3C847500461950 /* test */, + DF8439562F3C847600461950 /* testUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DF84393C2F3C847500461950 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DF843A542F3C8B4400461950 /* TestPlan.xctestplan in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DF8439552F3C847600461950 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DF84393A2F3C847500461950 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DF8439532F3C847600461950 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DF8439592F3C847600461950 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DF84393D2F3C847500461950 /* test */; + targetProxy = DF8439582F3C847600461950 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + DF84395F2F3C847600461950 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = P9KK452K8P; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DF8439602F3C847600461950 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = P9KK452K8P; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + DF8439622F3C847600461950 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P9KK452K8P; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = rxlab.test; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + DF8439632F3C847600461950 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P9KK452K8P; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = rxlab.test; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + DF8439682F3C847600461950 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P9KK452K8P; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = rxlab.testUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_TARGET_NAME = test; + }; + name = Debug; + }; + DF8439692F3C847600461950 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P9KK452K8P; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = rxlab.testUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_TARGET_NAME = test; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DF8439392F3C847500461950 /* Build configuration list for PBXProject "test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DF84395F2F3C847600461950 /* Debug */, + DF8439602F3C847600461950 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DF8439612F3C847600461950 /* Build configuration list for PBXNativeTarget "test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DF8439622F3C847600461950 /* Debug */, + DF8439632F3C847600461950 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DF8439672F3C847600461950 /* Build configuration list for PBXNativeTarget "testUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DF8439682F3C847600461950 /* Debug */, + DF8439692F3C847600461950 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + DF84396A2F3C848E00461950 /* XCLocalSwiftPackageReference "../../RxAuthSwift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../RxAuthSwift; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DF84396B2F3C848E00461950 /* RxAuthSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = RxAuthSwift; + }; + DF84396D2F3C848E00461950 /* RxAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + productName = RxAuthSwiftUI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DF8439362F3C847500461950 /* Project object */; +} diff --git a/test/test.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/test/test.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/test/test.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/test/test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/test/test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..1f8bb6b --- /dev/null +++ b/test/test.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "7dbe784e84e49ae4547eafc2067a194c8f04e90d607a72dc83ac739549dea8c5", + "pins" : [ + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181", + "version" : "1.9.1" + } + } + ], + "version" : 3 +} diff --git a/test/test.xcodeproj/xcshareddata/xcschemes/test.xcscheme b/test/test.xcodeproj/xcshareddata/xcschemes/test.xcscheme new file mode 100644 index 0000000..a753ea2 --- /dev/null +++ b/test/test.xcodeproj/xcshareddata/xcschemes/test.xcscheme @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test.xcodeproj/xcshareddata/xcschemes/testUITests.xcscheme b/test/test.xcodeproj/xcshareddata/xcschemes/testUITests.xcscheme new file mode 100644 index 0000000..72515db --- /dev/null +++ b/test/test.xcodeproj/xcshareddata/xcschemes/testUITests.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test/Assets.xcassets/AccentColor.colorset/Contents.json b/test/test/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/test/test/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/test/test/Assets.xcassets/AppIcon.appiconset/Contents.json b/test/test/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/test/test/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/test/test/Assets.xcassets/Contents.json b/test/test/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/test/test/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/test/test/ContentView.swift b/test/test/ContentView.swift new file mode 100644 index 0000000..4099222 --- /dev/null +++ b/test/test/ContentView.swift @@ -0,0 +1,75 @@ +// +// ContentView.swift +// test +// +// Created by Qiwei Li on 2/11/26. +// + +import RxAuthSwift +import RxAuthSwiftUI +import SwiftUI + +struct ContentView: View { + @Bindable var manager: OAuthManager + + var body: some View { + Group { + switch manager.authState { + case .unknown: + ProgressView("Loading...") + .task { + await manager.checkExistingAuth() + } + case .unauthenticated: + RxSignInView( + manager: manager, + appearance: RxSignInAppearance( + title: "RxAuth Test", + subtitle: "Sign in to test authentication" + ), + onAuthSuccess: { + print("Auth succeeded") + }, + onAuthFailed: { error in + print("Auth failed: \(error)") + } + ) + case .authenticated: + authenticatedView + } + } + } + + private var authenticatedView: some View { + VStack(spacing: 20) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 60)) + .foregroundStyle(.green) + + Text("Welcome!") + .accessibilityLabel("home-view-title") + .font(.largeTitle) + + if let user = manager.currentUser { + if let name = user.name { + Text(name) + .font(.title2) + } + if let email = user.email { + Text(email) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + Button("Sign Out") { + Task { + await manager.logout() + } + } + .buttonStyle(.borderedProminent) + .tint(.red) + } + .padding() + } +} diff --git a/test/test/testApp.swift b/test/test/testApp.swift new file mode 100644 index 0000000..cbdca8c --- /dev/null +++ b/test/test/testApp.swift @@ -0,0 +1,40 @@ +// +// testApp.swift +// test +// +// Created by Qiwei Li on 2/11/26. +// + +import RxAuthSwift +import SwiftUI + +@main +struct testApp: App { + @State private var manager: OAuthManager + + init() { + let configuration = RxAuthConfiguration( + issuer: "https://auth.rxlab.app", + clientID: "client_8605760939b8494c8bfe29c77ae7ee7f", + redirectURI: "rxauth://callback" + ) + + let resetAuth = ProcessInfo.processInfo.arguments.contains("--reset-auth") + let tokenStorage: TokenStorageProtocol? = resetAuth ? InMemoryTokenStorage() : nil + + if resetAuth { + try? KeychainTokenStorage(serviceName: configuration.keychainServiceName).clearAll() + } + + _manager = State(initialValue: OAuthManager( + configuration: configuration, + tokenStorage: tokenStorage + )) + } + + var body: some Scene { + WindowGroup { + ContentView(manager: manager) + } + } +} diff --git a/test/testUITests/helper/dotenv.swift b/test/testUITests/helper/dotenv.swift new file mode 100644 index 0000000..bd35352 --- /dev/null +++ b/test/testUITests/helper/dotenv.swift @@ -0,0 +1,129 @@ +// +// DotEnv.swift +// RxStorageUITests +// +// Helper to read environment variables from .env file +// + +import Foundation + +enum DotEnv { + /// Loads environment variables from a .env file + /// - Parameter path: Path to the .env file. If nil, searches in common locations. + /// - Returns: Dictionary of environment variables + static func load(from path: String? = nil) -> [String: String] { + let fileManager = FileManager.default + var envPath: String? + + if let path = path { + envPath = path + } else { + // Try common locations relative to the test bundle + let possiblePaths = [ + // Relative to project root (when running from Xcode) + URL(fileURLWithPath: #file) + .deletingLastPathComponent() // utils + .deletingLastPathComponent() // RxStorageUITests + .deletingLastPathComponent() // RxStorage + .appendingPathComponent(".env") + .path, + // Also check UITests directory + URL(fileURLWithPath: #file) + .deletingLastPathComponent() // utils + .deletingLastPathComponent() // RxStorageUITests + .appendingPathComponent(".env") + .path, + ] + + for path in possiblePaths { + if fileManager.fileExists(atPath: path) { + envPath = path + break + } + } + } + + guard let envPath = envPath else { + NSLog("⚠️ DotEnv: No .env file found") + return [:] + } + + return parse(filePath: envPath) + } + + /// Parses a .env file and returns key-value pairs + private static func parse(filePath: String) -> [String: String] { + var result: [String: String] = [:] + + guard let contents = try? String(contentsOfFile: filePath, encoding: .utf8) else { + NSLog("⚠️ DotEnv: Could not read file at \(filePath)") + return [:] + } + + NSLog("✅ DotEnv: Loaded .env from \(filePath)") + + let lines = contents.components(separatedBy: .newlines) + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespaces) + + // Skip empty lines and comments + if trimmed.isEmpty || trimmed.hasPrefix("#") { + continue + } + + // Split on first '=' only + guard let equalsIndex = trimmed.firstIndex(of: "=") else { + continue + } + + let key = String(trimmed[.. String? { + // First check .env file values if provided + if let envVars = envVars, let value = envVars[key] { + return value + } + + // Fall back to process environment (for CI/CD or scheme-based config) + if let envValue = ProcessInfo.processInfo.environment[key] { + NSLog("✅ DotEnv: Found \(key) in process environment") + return envValue + } + + NSLog("⚠️ DotEnv: \(key) not found in .env file or process environment") + return nil + } + + /// Loads environment variables, preferring .env file but falling back to process environment + /// This is useful for CI environments where .env file path resolution may fail + static func loadWithFallback() -> [String: String] { + var result = load() + + // If .env file loading returned empty, try to get known keys from environment + let knownKeys = ["TEST_EMAIL", "TEST_PASSWORD"] + for key in knownKeys { + if result[key] == nil, let envValue = ProcessInfo.processInfo.environment[key] { + result[key] = envValue + NSLog("✅ DotEnv: Loaded \(key) from process environment (fallback)") + } + } + + return result + } +} diff --git a/test/testUITests/helper/launch.swift b/test/testUITests/helper/launch.swift new file mode 100644 index 0000000..3077a4a --- /dev/null +++ b/test/testUITests/helper/launch.swift @@ -0,0 +1,19 @@ +// +// launch.swift +// test +// +// Created by Qiwei Li on 2/11/26. +// +import XCTest + +func launchApp() -> XCUIApplication { + let app = XCUIApplication() + + // --reset-auth flag will: + // 1. Clear stored tokens from Keychain + // 2. Use ephemeral Safari session (no cached credentials) + app.launchArguments = ["--reset-auth"] + + app.launch() + return app +} diff --git a/test/testUITests/helper/signIn.swift b/test/testUITests/helper/signIn.swift new file mode 100644 index 0000000..6858fdc --- /dev/null +++ b/test/testUITests/helper/signIn.swift @@ -0,0 +1,95 @@ +// +// signin.swift +// RxStorage +// +// Created by Qiwei Li on 2/2/26. +// +import os.log +import XCTest + +/// Use OSLog for better visibility in test output +private let logger = Logger(subsystem: "app.rxlab.RxStorageUITests", category: "signin") + +extension XCUIApplication { + func signInWithEmailAndPassword() throws { + // Load .env file and read credentials (with fallback to process environment for CI) + let envVars = DotEnv.loadWithFallback() + + let testEmail = DotEnv.get("TEST_EMAIL", from: envVars) ?? "test@rxlab.app" + NSLog("🔐 Using test email: \(testEmail)") + guard let testPassword = DotEnv.get("TEST_PASSWORD", from: envVars) else { + throw NSError(domain: "SigninError", code: 1, userInfo: [NSLocalizedDescriptionKey: "TEST_PASSWORD not found in .env file or environment"]) + } + NSLog("🔐 Using test password: \(testPassword)") + + NSLog("🔐 Starting sign-in flow with email: \(testEmail)") + logger.info("🔐 Starting sign-in flow with email: \(testEmail)") + + // Tap sign in button (by accessibility identifier) + let signInButton = buttons["sign-in-button"].firstMatch + NSLog("⏱️ Waiting for sign-in button...") + logger.info("⏱️ Waiting for sign-in button...") + XCTAssertTrue(signInButton.waitForExistence(timeout: 10), "Sign-in button did not appear") + NSLog("✅ Sign-in button found, tapping...") + logger.info("✅ Sign-in button found, tapping...") + signInButton.tap() + + // Give Safari time to launch + sleep(2) + + // Wait for Safari OAuth page to appear + #if os(iOS) + let safariViewServiceApp = XCUIApplication(bundleIdentifier: "com.apple.SafariViewService") + NSLog("⏱️ Waiting for Safari OAuth page to load...") + logger.info("⏱️ Waiting for Safari OAuth page to load...") + + // Wait for email field to appear (OAuth page loaded) + let emailField = safariViewServiceApp.textFields["you@example.com"].firstMatch + let passwordField = safariViewServiceApp.secureTextFields["Enter your password"].firstMatch + + // Use a longer timeout and provide better error message + let emailFieldExists = emailField.waitForExistence(timeout: 30) + NSLog("✅ Email field found, entering credentials...") + logger.info("✅ Email field found, entering credentials...") + + // Fill in credentials from environment + // WebView elements need extra handling for keyboard focus in CI + emailField.tap() + sleep(1) // Give WebView time to establish keyboard focus + // Type the email + emailField.typeText(testEmail) + NSLog("✅ Email entered") + logger.info("✅ Email entered") + + // Small delay before pressing Enter + sleep(1) + emailField.typeText("\n") // Press Enter to move to next field + #elseif os(macOS) + + let emailField = textFields["you@example.com"].firstMatch + let emailFieldExists = emailField.waitForExistence(timeout: 30) + XCTAssertTrue(emailFieldExists, "Failed to sign in and reach dashboard") + + let passwordField = self/*@START_MENU_TOKEN@*/ .secureTextFields["Enter your password"].firstMatch/*[[".groups",".secureTextFields[\"Password\"].firstMatch",".secureTextFields[\"Enter your password\"].firstMatch",".secureTextFields",".containing(.group, identifier: nil).firstMatch",".firstMatch"],[[[-1,2],[-1,1],[-1,3,2],[-1,0,1]],[[-1,2],[-1,1]],[[-1,5],[-1,4]]],[0]]@END_MENU_TOKEN@*/ + + emailField.click() + emailField.typeText(testEmail) + #endif + // WebView password field also needs focus handling + passwordField.tap() + sleep(1) // Give WebView time to establish keyboard focus + + passwordField.typeText(testPassword) + NSLog("✅ Password entered, submitting...") + logger.info("✅ Password entered, submitting...") + sleep(1) + passwordField.typeText("\n") // Press Enter to submit + + NSLog("✅ Sign-in form submitted, waiting for callback...") + logger.info("✅ Sign-in form submitted, waiting for callback...") + + // find dashboard-view + let exist = staticTexts["home-view-title"].waitForExistence(timeout: 30) + XCTAssertTrue(exist, "Failed to sign in and reach dashboard") + } +} diff --git a/test/testUITests/testUITests.swift b/test/testUITests/testUITests.swift new file mode 100644 index 0000000..c883a38 --- /dev/null +++ b/test/testUITests/testUITests.swift @@ -0,0 +1,16 @@ +// +// testUITests.swift +// testUITests +// +// Created by Qiwei Li on 2/11/26. +// + +import XCTest + +final class testUITests: XCTestCase { + @MainActor + func testSignInWithoutIssue() throws { + let app = launchApp() + try app.signInWithEmailAndPassword() + } +}