新增完整 UI Test 回归套件,并完善启动隔离逻辑。
按模块拆分 XCUITest 用例与 Support 基础设施,UI Test 运行时跳过推送与排队 WebSocket,并将 Podfile 最低版本对齐 iOS 16。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,4 +47,5 @@ fastlane/test_output
|
|||||||
# Env / secrets
|
# Env / secrets
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
suixinkanUITests/UITestCredentials.local.plist
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
iOS
|
iOS
|
||||||
- iOS App
|
- iOS App
|
||||||
- SwiftUI
|
- SwiftUI
|
||||||
- 最低支持 iOS 17
|
- 最低支持 iOS 16
|
||||||
- 只支持竖屏
|
- 只支持竖屏
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
Podfile
2
Podfile
@ -1,4 +1,4 @@
|
|||||||
platform :ios, '17.0'
|
platform :ios, '16.0'
|
||||||
|
|
||||||
target 'suixinkan' do
|
target 'suixinkan' do
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
|
|||||||
@ -25,6 +25,6 @@ SPEC CHECKSUMS:
|
|||||||
AMapLocation-NO-IDFA: 0fa6e7d5e42e10253f4437424d98966f53760862
|
AMapLocation-NO-IDFA: 0fa6e7d5e42e10253f4437424d98966f53760862
|
||||||
AMapSearch-NO-IDFA: 8e9d104032990a3b0cab66afb535299887c61047
|
AMapSearch-NO-IDFA: 8e9d104032990a3b0cab66afb535299887c61047
|
||||||
|
|
||||||
PODFILE CHECKSUM: 50778cfc2820db549075417f39146c17f4bd07f1
|
PODFILE CHECKSUM: baa972253fcebc312545f7592c48cbba632f1907
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
2
Pods/Manifest.lock
generated
2
Pods/Manifest.lock
generated
@ -25,6 +25,6 @@ SPEC CHECKSUMS:
|
|||||||
AMapLocation-NO-IDFA: 0fa6e7d5e42e10253f4437424d98966f53760862
|
AMapLocation-NO-IDFA: 0fa6e7d5e42e10253f4437424d98966f53760862
|
||||||
AMapSearch-NO-IDFA: 8e9d104032990a3b0cab66afb535299887c61047
|
AMapSearch-NO-IDFA: 8e9d104032990a3b0cab66afb535299887c61047
|
||||||
|
|
||||||
PODFILE CHECKSUM: 50778cfc2820db549075417f39146c17f4bd07f1
|
PODFILE CHECKSUM: baa972253fcebc312545f7592c48cbba632f1907
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
392
Pods/Pods.xcodeproj/project.pbxproj
generated
392
Pods/Pods.xcodeproj/project.pbxproj
generated
@ -768,86 +768,7 @@
|
|||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
0B9288E0B1AFAE5C85E5C0C3D1C1D8E6 /* Release */ = {
|
0606EC124017CF52B54FEB3096BC7E75 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReference = 2D9188A08F7F86FBF3EBB3CD93699D53 /* Pods-suixinkan.release.xcconfig */;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEFINES_MODULE = YES;
|
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
|
||||||
DYLIB_CURRENT_VERSION = 1;
|
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
|
||||||
ENABLE_MODULE_VERIFIER = NO;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
|
||||||
INFOPLIST_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan-Info.plist";
|
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@loader_path/Frameworks",
|
|
||||||
);
|
|
||||||
MACH_O_TYPE = staticlib;
|
|
||||||
MODULEMAP_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan.modulemap";
|
|
||||||
OTHER_LDFLAGS = "";
|
|
||||||
OTHER_LIBTOOLFLAGS = "";
|
|
||||||
PODS_ROOT = "$(SRCROOT)";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
|
||||||
VERSION_INFO_PREFIX = "";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
0B998EC7D708CA14F6D00FD60D2A1C98 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReference = 61B7C8006610229F8A3485704A3944CF /* Pods-suixinkan.debug.xcconfig */;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEFINES_MODULE = YES;
|
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
|
||||||
DYLIB_CURRENT_VERSION = 1;
|
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
|
||||||
ENABLE_MODULE_VERIFIER = NO;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
|
||||||
INFOPLIST_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan-Info.plist";
|
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@loader_path/Frameworks",
|
|
||||||
);
|
|
||||||
MACH_O_TYPE = staticlib;
|
|
||||||
MODULEMAP_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan.modulemap";
|
|
||||||
OTHER_LDFLAGS = "";
|
|
||||||
OTHER_LIBTOOLFLAGS = "";
|
|
||||||
PODS_ROOT = "$(SRCROOT)";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
|
||||||
VERSION_INFO_PREFIX = "";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
0FAFBB4DB9A284A5FAA328FEA1AF4FD9 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 3CE7B2EE0FE95BBD834825FA4D0571C5 /* Pods-suixinkanTests.debug.xcconfig */;
|
baseConfigurationReference = 3CE7B2EE0FE95BBD834825FA4D0571C5 /* Pods-suixinkanTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@ -865,7 +786,7 @@
|
|||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests-Info.plist";
|
INFOPLIST_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests-Info.plist";
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -886,6 +807,68 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
30E0B9EFD9A5C45D0D351231E81B30B3 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
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";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"POD_CONFIGURATION_RELEASE=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;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
STRIP_INSTALLED_PRODUCT = NO;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
SYMROOT = "${SRCROOT}/../build";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
33FF65CC939DB2ACD3BF5B4E6E6F56D6 /* Release */ = {
|
33FF65CC939DB2ACD3BF5B4E6E6F56D6 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 1C389829A9B7304C4C2F81E54D9281D4 /* AMapFoundation-NO-IDFA.release.xcconfig */;
|
baseConfigurationReference = 1C389829A9B7304C4C2F81E54D9281D4 /* AMapFoundation-NO-IDFA.release.xcconfig */;
|
||||||
@ -905,46 +888,6 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
443447B150CD11B46EEB5D882C8F21B7 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReference = A1C2AE0D141F894C0C841BDB91EA7784 /* Pods-suixinkanTests.release.xcconfig */;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEFINES_MODULE = YES;
|
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
|
||||||
DYLIB_CURRENT_VERSION = 1;
|
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
|
||||||
ENABLE_MODULE_VERIFIER = NO;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
|
||||||
INFOPLIST_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests-Info.plist";
|
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@loader_path/Frameworks",
|
|
||||||
);
|
|
||||||
MACH_O_TYPE = staticlib;
|
|
||||||
MODULEMAP_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.modulemap";
|
|
||||||
OTHER_LDFLAGS = "";
|
|
||||||
OTHER_LIBTOOLFLAGS = "";
|
|
||||||
PODS_ROOT = "$(SRCROOT)";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
|
||||||
VERSION_INFO_PREFIX = "";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
49EBBE60492138920A951F8BD7CD4013 /* Release */ = {
|
49EBBE60492138920A951F8BD7CD4013 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 0249ED7AD58489423B69253BB3F68C77 /* AMap3DMap-NO-IDFA.release.xcconfig */;
|
baseConfigurationReference = 0249ED7AD58489423B69253BB3F68C77 /* AMap3DMap-NO-IDFA.release.xcconfig */;
|
||||||
@ -982,71 +925,45 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
6A5C026ED62BBFE3380CD257EEFAEB20 /* Debug */ = {
|
59C25D381C381A4E53F13543611D1E41 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = A1C2AE0D141F894C0C841BDB91EA7784 /* Pods-suixinkanTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
DEFINES_MODULE = YES;
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
ENABLE_MODULE_VERIFIER = NO;
|
||||||
CLANG_WARN_COMMA = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
INFOPLIST_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests-Info.plist";
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
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;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"POD_CONFIGURATION_DEBUG=1",
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
MACH_O_TYPE = staticlib;
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
MODULEMAP_FILE = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.modulemap";
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
OTHER_LDFLAGS = "";
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
OTHER_LIBTOOLFLAGS = "";
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
PODS_ROOT = "$(SRCROOT)";
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
SDKROOT = iphoneos;
|
||||||
MTL_FAST_MATH = YES;
|
SKIP_INSTALL = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
VALIDATE_PRODUCT = YES;
|
||||||
STRIP_INSTALLED_PRODUCT = NO;
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
VERSION_INFO_PREFIX = "";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
SYMROOT = "${SRCROOT}/../build";
|
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Release;
|
||||||
};
|
};
|
||||||
7BB3D81AAC9EB818C249626A41588C41 /* Debug */ = {
|
7BB3D81AAC9EB818C249626A41588C41 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@ -1066,6 +983,45 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
8076472B15CAFA346B2FBF304BD0B32C /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 61B7C8006610229F8A3485704A3944CF /* Pods-suixinkan.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
ENABLE_MODULE_VERIFIER = NO;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
INFOPLIST_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan-Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
MACH_O_TYPE = staticlib;
|
||||||
|
MODULEMAP_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan.modulemap";
|
||||||
|
OTHER_LDFLAGS = "";
|
||||||
|
OTHER_LIBTOOLFLAGS = "";
|
||||||
|
PODS_ROOT = "$(SRCROOT)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
A5889394557E95B280BBA784CBC27862 /* Debug */ = {
|
A5889394557E95B280BBA784CBC27862 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 50416CD761E5B67B024889A9055280A1 /* AMapLocation-NO-IDFA.debug.xcconfig */;
|
baseConfigurationReference = 50416CD761E5B67B024889A9055280A1 /* AMapLocation-NO-IDFA.debug.xcconfig */;
|
||||||
@ -1140,7 +1096,47 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
E873CA97BD124B21839AF13C9BE1CD18 /* Release */ = {
|
EB4999FBC2DB974780AA976726FD20A2 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 2D9188A08F7F86FBF3EBB3CD93699D53 /* Pods-suixinkan.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
ENABLE_MODULE_VERIFIER = NO;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
INFOPLIST_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan-Info.plist";
|
||||||
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
MACH_O_TYPE = staticlib;
|
||||||
|
MODULEMAP_FILE = "Target Support Files/Pods-suixinkan/Pods-suixinkan.modulemap";
|
||||||
|
OTHER_LDFLAGS = "";
|
||||||
|
OTHER_LIBTOOLFLAGS = "";
|
||||||
|
PODS_ROOT = "$(SRCROOT)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
F4FF6A0D1970CA9705974E3CB2134802 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
@ -1175,13 +1171,16 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"POD_CONFIGURATION_RELEASE=1",
|
"POD_CONFIGURATION_DEBUG=1",
|
||||||
|
"DEBUG=1",
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@ -1190,17 +1189,18 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRIP_INSTALLED_PRODUCT = NO;
|
STRIP_INSTALLED_PRODUCT = NO;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
SYMROOT = "${SRCROOT}/../build";
|
SYMROOT = "${SRCROOT}/../build";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Debug;
|
||||||
};
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
@ -1208,8 +1208,8 @@
|
|||||||
4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = {
|
4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
6A5C026ED62BBFE3380CD257EEFAEB20 /* Debug */,
|
F4FF6A0D1970CA9705974E3CB2134802 /* Debug */,
|
||||||
E873CA97BD124B21839AF13C9BE1CD18 /* Release */,
|
30E0B9EFD9A5C45D0D351231E81B30B3 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@ -1244,8 +1244,8 @@
|
|||||||
9D12533D25351890E8822DC48F49289E /* Build configuration list for PBXNativeTarget "Pods-suixinkanTests" */ = {
|
9D12533D25351890E8822DC48F49289E /* Build configuration list for PBXNativeTarget "Pods-suixinkanTests" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
0FAFBB4DB9A284A5FAA328FEA1AF4FD9 /* Debug */,
|
0606EC124017CF52B54FEB3096BC7E75 /* Debug */,
|
||||||
443447B150CD11B46EEB5D882C8F21B7 /* Release */,
|
59C25D381C381A4E53F13543611D1E41 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@ -1262,8 +1262,8 @@
|
|||||||
FCDBF877017B428DCC77941BF6BFCE55 /* Build configuration list for PBXNativeTarget "Pods-suixinkan" */ = {
|
FCDBF877017B428DCC77941BF6BFCE55 /* Build configuration list for PBXNativeTarget "Pods-suixinkan" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
0B998EC7D708CA14F6D00FD60D2A1C98 /* Debug */,
|
8076472B15CAFA346B2FBF304BD0B32C /* Debug */,
|
||||||
0B9288E0B1AFAE5C85E5C0C3D1C1D8E6 /* Release */,
|
EB4999FBC2DB974780AA976726FD20A2 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|||||||
@ -38,6 +38,12 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
|||||||
10. 明确 token 失效时清空 token 和账号快照,回到登录页。
|
10. 明确 token 失效时清空 token 和账号快照,回到登录页。
|
||||||
11. 普通网络失败时保留本地登录态,使用账号快照进入主界面。
|
11. 普通网络失败时保留本地登录态,使用账号快照进入主界面。
|
||||||
|
|
||||||
|
## UI Test 启动约定
|
||||||
|
|
||||||
|
- `AppUITestLaunchState` 在收到 `-suixinkan-ui-tests` 时跳过推送注册和排队 WebSocket,避免系统弹窗干扰自动化。
|
||||||
|
- `-suixinkan-ui-tests-reset-state` 用于冷启动清理 Keychain 与 UserDefaults。
|
||||||
|
- 详细运行方式见 `suixinkanUITests/README.md`。
|
||||||
|
|
||||||
## 登录和退出
|
## 登录和退出
|
||||||
|
|
||||||
登录完成由 `AuthSessionCoordinator.completeLogin` 统一处理:
|
登录完成由 `AuthSessionCoordinator.completeLogin` 统一处理:
|
||||||
|
|||||||
@ -7,10 +7,19 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// UI 测试启动状态清理,仅在测试进程显式传入启动参数时执行。
|
/// UI 测试启动状态,仅在测试进程显式传入启动参数时生效。
|
||||||
enum AppUITestLaunchState {
|
enum AppUITestLaunchState {
|
||||||
|
/// UI Test 主开关,测试进程统一传入。
|
||||||
|
static let uiTestsArgument = "-suixinkan-ui-tests"
|
||||||
|
/// 冷启动前清理本地登录缓存。
|
||||||
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
||||||
|
|
||||||
|
/// 当前进程是否由 XCUITest 启动。
|
||||||
|
static var isRunningUITests: Bool {
|
||||||
|
ProcessInfo.processInfo.arguments.contains(uiTestsArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按需在冷启动时清理 token、账号快照和偏好设置。
|
||||||
static func resetIfNeeded(arguments: [String] = ProcessInfo.processInfo.arguments) {
|
static func resetIfNeeded(arguments: [String] = ProcessInfo.processInfo.arguments) {
|
||||||
guard arguments.contains(resetArgument) else { return }
|
guard arguments.contains(resetArgument) else { return }
|
||||||
|
|
||||||
|
|||||||
@ -146,7 +146,7 @@ struct RootView: View {
|
|||||||
toastCenter: toastCenter
|
toastCenter: toastCenter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if appSession.isLoggedIn {
|
if appSession.isLoggedIn, !AppUITestLaunchState.isRunningUITests {
|
||||||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,6 +161,7 @@ struct RootView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { newPhase in
|
.onChange(of: scenePhase) { newPhase in
|
||||||
|
guard !AppUITestLaunchState.isRunningUITests else { return }
|
||||||
if appSession.isLoggedIn {
|
if appSession.isLoggedIn {
|
||||||
scenicQueueRuntime.update(
|
scenicQueueRuntime.update(
|
||||||
api: scenicQueueAPI,
|
api: scenicQueueAPI,
|
||||||
@ -173,6 +174,7 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: accountContext.currentScenic?.id) { _ in
|
.onChange(of: accountContext.currentScenic?.id) { _ in
|
||||||
|
guard !AppUITestLaunchState.isRunningUITests else { return }
|
||||||
if appSession.isLoggedIn {
|
if appSession.isLoggedIn {
|
||||||
scenicQueueRuntime.update(
|
scenicQueueRuntime.update(
|
||||||
api: scenicQueueAPI,
|
api: scenicQueueAPI,
|
||||||
@ -185,7 +187,9 @@ struct RootView: View {
|
|||||||
.onChange(of: appSession.phase) { phase in
|
.onChange(of: appSession.phase) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
case .loggedIn:
|
case .loggedIn:
|
||||||
|
if !AppUITestLaunchState.isRunningUITests {
|
||||||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||||||
|
}
|
||||||
case .loggedOut:
|
case .loggedOut:
|
||||||
accountContext.reset()
|
accountContext.reset()
|
||||||
permissionContext.reset()
|
permissionContext.reset()
|
||||||
|
|||||||
25
suixinkanUITests/Auth/AuthFlowUITests.swift
Normal file
25
suixinkanUITests/Auth/AuthFlowUITests.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// AuthFlowUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class AuthFlowUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试完整登录后可进入主 Tab。
|
||||||
|
func testLoginReachesMainTabs() {
|
||||||
|
XCTAssertTrue(app.isOnMainTabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试登录后默认能访问首页和我的 Tab。
|
||||||
|
func testLoggedInUserCanSwitchPrimaryTabs() {
|
||||||
|
TabBarNavigator.select(.home, app: app)
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.application.staticTexts["常用应用"].waitForExistence(timeout: 8)
|
||||||
|
|| app.application.staticTexts["请选择景区"].exists
|
||||||
|
)
|
||||||
|
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("个人信息", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
suixinkanUITests/Auth/LoginSmokeUITests.swift
Normal file
60
suixinkanUITests/Auth/LoginSmokeUITests.swift
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// LoginSmokeUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class LoginSmokeUITests: XCTestCase {
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试冷启动会展示登录页关键控件。
|
||||||
|
func testFreshLaunchShowsLoginScreen() {
|
||||||
|
let app = SuixinkanApp(resetState: true).launch(waitForLoading: false)
|
||||||
|
|
||||||
|
XCTAssertTrue(app.application.staticTexts["login.title"].waitForExistence(timeout: 8))
|
||||||
|
XCTAssertTrue(app.application.textFields["login.username"].exists)
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.application.secureTextFields["login.password"].exists
|
||||||
|
|| app.application.textFields["login.password"].exists
|
||||||
|
)
|
||||||
|
XCTAssertTrue(app.application.buttons["login.submit"].exists)
|
||||||
|
XCTAssertTrue(app.application.buttons["login.userAgreement"].exists)
|
||||||
|
XCTAssertTrue(app.application.buttons["login.privacyPolicy"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试未勾选协议时无法直接登录。
|
||||||
|
func testLoginRequiresPrivacyAgreement() throws {
|
||||||
|
let app = SuixinkanApp(resetState: true).launch(waitForLoading: false)
|
||||||
|
guard UITestLaunchConfiguration.credentials != nil else {
|
||||||
|
throw XCTSkip("缺少 UI Test 账号。")
|
||||||
|
}
|
||||||
|
|
||||||
|
let usernameField = app.application.textFields["login.username"]
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 8))
|
||||||
|
XCTAssertFalse(app.application.buttons["login.submit"].isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试空密码不会离开登录页。
|
||||||
|
func testEmptyPasswordDoesNotLeaveLoginScreen() throws {
|
||||||
|
let app = SuixinkanApp(resetState: true).launch(waitForLoading: false)
|
||||||
|
let credentials = try UITestLaunchConfiguration.requireCredentials()
|
||||||
|
|
||||||
|
let usernameField = app.application.textFields["login.username"]
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 8))
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText(credentials.phone)
|
||||||
|
|
||||||
|
let privacyToggle = app.application.buttons["login.privacy"]
|
||||||
|
if privacyToggle.exists {
|
||||||
|
privacyToggle.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.application.buttons["login.submit"].tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear(timeout: 5)
|
||||||
|
XCTAssertTrue(app.isOnLoginScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
suixinkanUITests/Home/HomeRoutesUITests.swift
Normal file
33
suixinkanUITests/Home/HomeRoutesUITests.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// HomeRoutesUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class HomeRoutesUITests: AuthenticatedUITestCase {
|
||||||
|
/// 参数化遍历首页调试菜单,覆盖去重后的全部 Home 路由目标页。
|
||||||
|
func testAllHomeRoutesFromDebugMenu() {
|
||||||
|
for entry in UITestHomeMenuCatalog.entries {
|
||||||
|
XCTContext.runActivity(named: entry.menuTitle) { _ in
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: entry.menuTitle, app: app)
|
||||||
|
UITestExpectationVerifier.verify(entry.expectation, app: app)
|
||||||
|
restoreAfterRoute(entry.expectation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据打开结果恢复到调试菜单,便于继续下一项。
|
||||||
|
private func restoreAfterRoute(_ expectation: UITestHomeMenuCatalog.Expectation) {
|
||||||
|
switch expectation {
|
||||||
|
case .tab:
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
case .staticText:
|
||||||
|
BackNavigator.dismissCustomTopBarIfNeeded(app: app)
|
||||||
|
DebugMenuNavigator.returnToMenu(app: app)
|
||||||
|
default:
|
||||||
|
DebugMenuNavigator.returnToMenu(app: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// LoginSmokeUITests.swift
|
|
||||||
// suixinkanUITests
|
|
||||||
//
|
|
||||||
// Created by Codex on 2026/6/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class LoginSmokeUITests: XCTestCase {
|
|
||||||
override func setUp() {
|
|
||||||
super.setUp()
|
|
||||||
continueAfterFailure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func testFreshLaunchShowsLoginScreen() {
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launchArguments.append("-suixinkan-ui-tests-reset-state")
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
XCTAssertTrue(app.staticTexts["login.title"].waitForExistence(timeout: 5))
|
|
||||||
XCTAssertTrue(app.textFields["login.username"].exists)
|
|
||||||
XCTAssertTrue(app.secureTextFields["login.password"].exists || app.textFields["login.password"].exists)
|
|
||||||
XCTAssertTrue(app.buttons["login.submit"].exists)
|
|
||||||
XCTAssertTrue(app.buttons["login.userAgreement"].exists)
|
|
||||||
XCTAssertTrue(app.buttons["login.privacyPolicy"].exists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
suixinkanUITests/Modules/InviteUITests.swift
Normal file
26
suixinkanUITests/Modules/InviteUITests.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// InviteUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class InviteUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试邀请页与邀请记录页。
|
||||||
|
func testInviteAndRecordPages() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "注册邀请", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("摄影师邀请", in: app)
|
||||||
|
|
||||||
|
let recordEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '邀请记录'")).firstMatch
|
||||||
|
if recordEntry.waitForExistence(timeout: 6) {
|
||||||
|
recordEntry.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("邀请记录", in: app)
|
||||||
|
} else {
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "邀请记录", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("邀请记录", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
suixinkanUITests/Modules/LiveUITests.swift
Normal file
29
suixinkanUITests/Modules/LiveUITests.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// LiveUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class LiveUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试直播管理列表与创建页。
|
||||||
|
func testLiveManagementAndCreatePage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "直播管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("直播管理", in: app)
|
||||||
|
|
||||||
|
let addButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '添加'")).firstMatch
|
||||||
|
if addButton.waitForExistence(timeout: 6) {
|
||||||
|
addButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("添加直播", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试直播相册页可打开。
|
||||||
|
func testLiveAlbumPage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "直播相册", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("直播相册", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
suixinkanUITests/Modules/LocationReportUITests.swift
Normal file
26
suixinkanUITests/Modules/LocationReportUITests.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// LocationReportUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class LocationReportUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试位置上报与历史记录页。
|
||||||
|
func testLocationReportAndHistory() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "位置上报", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("位置上报", in: app)
|
||||||
|
|
||||||
|
let historyEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '历史'")).firstMatch
|
||||||
|
if historyEntry.waitForExistence(timeout: 6) {
|
||||||
|
historyEntry.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("位置上报历史", in: app)
|
||||||
|
} else {
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "定位上报历史", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("位置上报历史", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkanUITests/Modules/MessageCenterUITests.swift
Normal file
21
suixinkanUITests/Modules/MessageCenterUITests.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// MessageCenterUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class MessageCenterUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试消息中心列表可展示并尝试进入详情。
|
||||||
|
func testMessageCenterListAndDetail() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "消息中心", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("消息中心", in: app)
|
||||||
|
|
||||||
|
let firstCell = app.application.cells.element(boundBy: 0)
|
||||||
|
if firstCell.waitForExistence(timeout: 8) {
|
||||||
|
firstCell.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("消息详情", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
suixinkanUITests/Modules/PilotCertificationUITests.swift
Normal file
28
suixinkanUITests/Modules/PilotCertificationUITests.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// PilotCertificationUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class PilotCertificationUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试飞手认证表单校验与验证码按钮。
|
||||||
|
func testPilotCertificationValidation() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "飞手认证", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("飞手认证", in: app)
|
||||||
|
|
||||||
|
let submitButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '提交'")).firstMatch
|
||||||
|
if submitButton.waitForExistence(timeout: 6) {
|
||||||
|
submitButton.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '请'")).firstMatch
|
||||||
|
.waitForExistence(timeout: 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
let smsButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '验证码'")).firstMatch
|
||||||
|
XCTAssertTrue(smsButton.exists)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
suixinkanUITests/Modules/ProjectsUITests.swift
Normal file
29
suixinkanUITests/Modules/ProjectsUITests.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// ProjectsUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ProjectsUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试摄影师项目列表与新建页。
|
||||||
|
func testProjectManagementCreatePage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "项目管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("项目管理", in: app)
|
||||||
|
|
||||||
|
let createButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '新建'")).firstMatch
|
||||||
|
if createButton.waitForExistence(timeout: 6) {
|
||||||
|
createButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("新建项目", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试店铺项目管理页可打开。
|
||||||
|
func testStoreProjectManagementPage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "店铺项目管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("店铺项目管理", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkanUITests/Modules/PunchPointUITests.swift
Normal file
21
suixinkanUITests/Modules/PunchPointUITests.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// PunchPointUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class PunchPointUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试打卡点列表与新建页。
|
||||||
|
func testPunchPointListAndCreatePage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "打卡点管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("打卡点管理", in: app)
|
||||||
|
|
||||||
|
let createButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '新增'")).firstMatch
|
||||||
|
if createButton.waitForExistence(timeout: 6) {
|
||||||
|
createButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("新增打卡点", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkanUITests/Modules/QueueManagementUITests.swift
Normal file
21
suixinkanUITests/Modules/QueueManagementUITests.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// QueueManagementUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class QueueManagementUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试排队管理首页与设置页。
|
||||||
|
func testQueueManagementAndSettings() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "排队管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("排队管理", in: app)
|
||||||
|
|
||||||
|
let settingsEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '设置'")).firstMatch
|
||||||
|
if settingsEntry.waitForExistence(timeout: 6) {
|
||||||
|
settingsEntry.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("排队设置", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkanUITests/Modules/ScheduleUITests.swift
Normal file
21
suixinkanUITests/Modules/ScheduleUITests.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// ScheduleUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ScheduleUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试日程管理页与新增排班页。
|
||||||
|
func testScheduleManagementAndAddPage() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "日程管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("日程管理", in: app)
|
||||||
|
|
||||||
|
let addButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '添加'")).firstMatch
|
||||||
|
if addButton.waitForExistence(timeout: 6) {
|
||||||
|
addButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("添加日程", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
suixinkanUITests/Modules/TasksUITests.swift
Normal file
32
suixinkanUITests/Modules/TasksUITests.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// TasksUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class TasksUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试任务列表可进入详情,发布任务页可打开。
|
||||||
|
func testTaskListDetailAndCreateValidation() throws {
|
||||||
|
openTaskManagement()
|
||||||
|
|
||||||
|
let firstCell = app.application.cells.element(boundBy: 0)
|
||||||
|
if firstCell.waitForExistence(timeout: 10) {
|
||||||
|
firstCell.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("任务详情", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
} else {
|
||||||
|
throw XCTSkip("测试账号暂无任务数据,跳过详情流测试。")
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "发布任务", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("发布任务", in: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openTaskManagement() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "任务管理", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("任务管理", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
suixinkanUITests/Modules/WalletUITests.swift
Normal file
42
suixinkanUITests/Modules/WalletUITests.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// WalletUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class WalletUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试钱包首页与提现表单校验。
|
||||||
|
func testWalletWithdrawValidation() {
|
||||||
|
openWallet()
|
||||||
|
|
||||||
|
app.application.staticTexts["提现"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("提现申请", in: app)
|
||||||
|
|
||||||
|
let submitButton = app.application.buttons["提交提现申请"]
|
||||||
|
XCTAssertTrue(submitButton.waitForExistence(timeout: 6))
|
||||||
|
submitButton.tap()
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.application.staticTexts["请输入提现金额"].waitForExistence(timeout: 4)
|
||||||
|
|| app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '金额'")).firstMatch.exists
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试银行卡与积分兑换二级页可打开。
|
||||||
|
func testWalletSecondaryPages() {
|
||||||
|
openWallet()
|
||||||
|
|
||||||
|
app.application.staticTexts["银行卡"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("银行卡设置", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["积分兑换"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("积分兑换", in: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openWallet() {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "我的钱包", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("我的钱包", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
suixinkanUITests/Orders/OrdersFlowUITests.swift
Normal file
76
suixinkanUITests/Orders/OrdersFlowUITests.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// OrdersFlowUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class OrdersFlowUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试门店订单列表可进入详情页。
|
||||||
|
func testStoreOrderDetailFlow() throws {
|
||||||
|
TabBarNavigator.select(.orders, app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["订单管理"].tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
|
||||||
|
let firstOrderCard = app.application.otherElements
|
||||||
|
.matching(NSPredicate(format: "identifier BEGINSWITH 'orders.store.card.'"))
|
||||||
|
.firstMatch
|
||||||
|
if !firstOrderCard.waitForExistence(timeout: 10) {
|
||||||
|
throw XCTSkip("测试账号当前景区下暂无门店订单,跳过详情流测试。")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstOrderCard.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("订单详情", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试核销订单列表可进入详情页。
|
||||||
|
func testWriteOffOrderDetailFlow() throws {
|
||||||
|
TabBarNavigator.select(.orders, app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["核销订单"].tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
|
||||||
|
let firstDetailButton = app.application.buttons
|
||||||
|
.matching(NSPredicate(format: "identifier BEGINSWITH 'orders.writeoff.detail.'"))
|
||||||
|
.firstMatch
|
||||||
|
if !firstDetailButton.waitForExistence(timeout: 10) {
|
||||||
|
throw XCTSkip("测试账号当前景区下暂无核销订单,跳过详情流测试。")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstDetailButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("核销订单详情", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试押金订单链路可进入列表与详情。
|
||||||
|
func testDepositOrderFlow() throws {
|
||||||
|
DebugMenuNavigator.open(app: app)
|
||||||
|
DebugMenuNavigator.tapMenu(title: "押金订单详情", app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("押金订单", in: app)
|
||||||
|
|
||||||
|
let firstCell = app.application.cells.element(boundBy: 0)
|
||||||
|
if firstCell.waitForExistence(timeout: 8) {
|
||||||
|
firstCell.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("押金订单详情", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
} else {
|
||||||
|
throw XCTSkip("测试账号暂无押金订单,跳过详情流测试。")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试扫码页可打开并关闭。
|
||||||
|
func testScannerCanOpenAndClose() {
|
||||||
|
TabBarNavigator.select(.orders, app: app)
|
||||||
|
|
||||||
|
let scanButton = app.application.buttons["扫码核销"]
|
||||||
|
XCTAssertTrue(scanButton.waitForExistence(timeout: 6))
|
||||||
|
scanButton.tap()
|
||||||
|
|
||||||
|
let closeButton = app.application.buttons["scanner.close"]
|
||||||
|
XCTAssertTrue(closeButton.waitForExistence(timeout: 8))
|
||||||
|
closeButton.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("订单", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
suixinkanUITests/Profile/ProfileRoutesUITests.swift
Normal file
49
suixinkanUITests/Profile/ProfileRoutesUITests.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// ProfileRoutesUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ProfileRoutesUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试系统设置页及协议入口可打开。
|
||||||
|
func testSettingsAndAgreementPages() {
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["系统设置"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("设置", in: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["关于我们"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("关于我们", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["用户协议"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("用户协议", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["隐私政策"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("隐私政策", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试实名认证页可打开并展示表单。
|
||||||
|
func testRealNameAuthPage() {
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["认证状态"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("实名认证", in: app)
|
||||||
|
XCTAssertTrue(app.application.textFields.count > 0 || app.application.secureTextFields.count > 0)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试账号切换页可打开并取消返回。
|
||||||
|
func testAccountSwitchPage() {
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
|
||||||
|
app.application.staticTexts["当前账号"].tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("账号切换", in: app)
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("个人信息", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
88
suixinkanUITests/README.md
Normal file
88
suixinkanUITests/README.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# suixinkan UI Tests
|
||||||
|
|
||||||
|
本目录包含 App 的 XCUITest 回归套件,通过 **api-test 真实接口** 验证页面可达性与关键业务流程。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 测试代码仅位于 `suixinkanUITests/`,App 侧仅扩展 `suixinkan/App/` 基础设施(如 `AppUITestLaunchState`)。
|
||||||
|
- **不修改** `Features/`、`Core/` 业务代码,不在 App 内注入 Mock 网络。
|
||||||
|
- DEBUG 构建下通过「我的 → 首页调试」批量覆盖首页路由。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
在 Scheme **Environment Variables** 或 `xcodebuild` 中配置:
|
||||||
|
|
||||||
|
| 变量 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SUI_TEST_PHONE` | 是 | 登录手机号 |
|
||||||
|
| `SUI_TEST_PASSWORD` | 是 | 登录密码 |
|
||||||
|
| `SUI_TEST_ACCOUNT_NAME` | 否 | 多账号选择页要点的账号展示名 |
|
||||||
|
|
||||||
|
**本机快捷配置(推荐):** 在 `suixinkanUITests/UITestCredentials.local.plist` 中配置 `SUI_TEST_PHONE`、`SUI_TEST_PASSWORD` 和可选的 `SUI_TEST_ACCOUNT_NAME`。该文件已加入 `.gitignore`,不会提交到 Git。环境变量优先级高于本地 plist。
|
||||||
|
|
||||||
|
缺少账号时,需要登录的用例会自动 `XCTSkip`。
|
||||||
|
|
||||||
|
已登录用例默认复用 Keychain 会话,每个测试类只完整登录一次;仅 `LoginSmokeUITests` 会显式清空本地状态。
|
||||||
|
|
||||||
|
## 测试数据约定(api-test)
|
||||||
|
|
||||||
|
以下数据建议为 UI Test 专用账号预置,否则部分「列表 → 详情」用例会跳过:
|
||||||
|
|
||||||
|
- 至少 1 条门店订单、1 条核销订单
|
||||||
|
- 至少 1 条任务、项目、打卡点、押金订单(可选)
|
||||||
|
- 账号需有景区/门店上下文
|
||||||
|
|
||||||
|
## 运行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 仅跑 UI Tests(需先配置环境变量)
|
||||||
|
xcodebuild test \
|
||||||
|
-workspace suixinkan.xcworkspace \
|
||||||
|
-scheme suixinkan \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 17' \
|
||||||
|
-only-testing:suixinkanUITests
|
||||||
|
|
||||||
|
# 带环境变量示例
|
||||||
|
SUI_TEST_PHONE=13800138000 \
|
||||||
|
SUI_TEST_PASSWORD='your-password' \
|
||||||
|
xcodebuild test \
|
||||||
|
-workspace suixinkan.xcworkspace \
|
||||||
|
-scheme suixinkan \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 17' \
|
||||||
|
-only-testing:suixinkanUITests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动参数
|
||||||
|
|
||||||
|
UI Test 进程会自动传入:
|
||||||
|
|
||||||
|
- `-suixinkan-ui-tests`:主开关,App 侧跳过推送注册与排队 WebSocket
|
||||||
|
- `-suixinkan-ui-tests-reset-state`:冷启动清理 Keychain / UserDefaults
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
suixinkanUITests/
|
||||||
|
Support/ # 启动配置、登录、导航、断言
|
||||||
|
Auth/ # 登录冒烟与完整登录流
|
||||||
|
Shell/ # 主 Tab
|
||||||
|
Home/ # 首页调试菜单全路由
|
||||||
|
Profile/ # 我的二级页
|
||||||
|
Orders/ # 订单深层流
|
||||||
|
Modules/ # 各业务模块交互
|
||||||
|
```
|
||||||
|
|
||||||
|
## 降级 / 跳过项
|
||||||
|
|
||||||
|
以下场景在模拟器 UI Test 中仅验证「可打开 / 可关闭」,不做完整硬件或上传闭环:
|
||||||
|
|
||||||
|
- 真机摄像头扫码内容识别
|
||||||
|
- 直播推流预览与音视频专项
|
||||||
|
- 高德地图完整选点
|
||||||
|
- OSS 真实文件上传(可选只验证选图 Sheet)
|
||||||
|
|
||||||
|
## 新增页面时
|
||||||
|
|
||||||
|
1. 在 `Support/UITestHomeMenuCatalog.swift` 补充调试菜单覆盖项(如有首页入口)。
|
||||||
|
2. 在对应 `Modules/*UITests.swift` 增加关键交互用例。
|
||||||
|
3. 优先使用已有 `accessibilityIdentifier`、导航栏标题和稳定文案定位,不修改 Features 视图。
|
||||||
28
suixinkanUITests/Shell/MainTabUITests.swift
Normal file
28
suixinkanUITests/Shell/MainTabUITests.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// MainTabUITests.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class MainTabUITests: AuthenticatedUITestCase {
|
||||||
|
/// 测试四个主 Tab 均可切换且展示核心标题。
|
||||||
|
func testAllMainTabsAreReachable() {
|
||||||
|
TabBarNavigator.select(.home, app: app)
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.application.staticTexts["常用应用"].waitForExistence(timeout: 8)
|
||||||
|
|| app.application.staticTexts["请选择景区"].exists
|
||||||
|
)
|
||||||
|
|
||||||
|
TabBarNavigator.select(.orders, app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("订单", in: app)
|
||||||
|
NavigationAssertions.waitForStaticText("订单管理", in: app)
|
||||||
|
|
||||||
|
TabBarNavigator.select(.statistics, app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("数据", in: app)
|
||||||
|
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("个人信息", in: app)
|
||||||
|
NavigationAssertions.waitForStaticText("系统设置", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
suixinkanUITests/Support/AuthenticatedUITestCase.swift
Normal file
42
suixinkanUITests/Support/AuthenticatedUITestCase.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// AuthenticatedUITestCase.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 需要已登录态的 UI Test 基类,每个测试类只启动并登录一次。
|
||||||
|
class AuthenticatedUITestCase: XCTestCase {
|
||||||
|
private static var sharedApp: SuixinkanApp?
|
||||||
|
|
||||||
|
var app: SuixinkanApp!
|
||||||
|
|
||||||
|
override class func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
guard sharedApp == nil else { return }
|
||||||
|
guard UITestLaunchConfiguration.credentials != nil else { return }
|
||||||
|
|
||||||
|
sharedApp = try? UITestAuthenticatedSession.launchLoggedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func tearDown() {
|
||||||
|
sharedApp?.application.terminate()
|
||||||
|
sharedApp = nil
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
try super.setUpWithError()
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
guard let sharedApp = Self.sharedApp else {
|
||||||
|
throw XCTSkip("缺少 UI Test 账号或登录失败。")
|
||||||
|
}
|
||||||
|
|
||||||
|
app = sharedApp
|
||||||
|
app.application.activate()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
}
|
||||||
34
suixinkanUITests/Support/BackNavigator.swift
Normal file
34
suixinkanUITests/Support/BackNavigator.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// BackNavigator.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 导航返回封装。
|
||||||
|
enum BackNavigator {
|
||||||
|
/// 通过导航栏返回按钮回到上一页。
|
||||||
|
static func goBack(app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) {
|
||||||
|
let navigationBar = app.application.navigationBars.element(boundBy: 0)
|
||||||
|
guard navigationBar.exists else {
|
||||||
|
XCTFail("当前页面没有可返回的导航栏。", file: file, line: line)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let backButton = navigationBar.buttons.element(boundBy: 0)
|
||||||
|
XCTAssertTrue(backButton.waitForExistence(timeout: 4), "未找到返回按钮。", file: file, line: line)
|
||||||
|
backButton.tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭自定义顶栏页面(如全部功能页)。
|
||||||
|
static func dismissCustomTopBarIfNeeded(app: SuixinkanApp) {
|
||||||
|
let backCandidates = app.application.buttons.matching(NSPredicate(format: "label CONTAINS 'chevron.left'"))
|
||||||
|
if backCandidates.count > 0 {
|
||||||
|
backCandidates.element(boundBy: 0).tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
suixinkanUITests/Support/DebugMenuNavigator.swift
Normal file
42
suixinkanUITests/Support/DebugMenuNavigator.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// DebugMenuNavigator.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// DEBUG 首页菜单调试页导航封装。
|
||||||
|
enum DebugMenuNavigator {
|
||||||
|
/// 进入「我的 → 首页调试」列表。
|
||||||
|
static func open(app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) {
|
||||||
|
TabBarNavigator.select(.profile, app: app)
|
||||||
|
let debugEntry = app.application.staticTexts["首页调试"]
|
||||||
|
XCTAssertTrue(debugEntry.waitForExistence(timeout: 8), "未找到首页调试入口。", file: file, line: line)
|
||||||
|
debugEntry.tap()
|
||||||
|
NavigationAssertions.waitForNavigationTitle("首页菜单调试", in: app, file: file, line: line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在调试菜单中点击指定标题。
|
||||||
|
static func tapMenu(
|
||||||
|
title: String,
|
||||||
|
app: SuixinkanApp,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) {
|
||||||
|
let cell = app.application.staticTexts[title]
|
||||||
|
XCTAssertTrue(cell.waitForExistence(timeout: 8), "调试菜单未找到:\(title)", file: file, line: line)
|
||||||
|
cell.tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回调试菜单列表。
|
||||||
|
static func returnToMenu(app: SuixinkanApp) {
|
||||||
|
if app.application.navigationBars["首页菜单调试"].exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BackNavigator.goBack(app: app)
|
||||||
|
NavigationAssertions.waitForNavigationTitle("首页菜单调试", in: app)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
suixinkanUITests/Support/LoginFlow.swift
Normal file
104
suixinkanUITests/Support/LoginFlow.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// LoginFlow.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 登录与多账号选择流程封装。
|
||||||
|
enum LoginFlow {
|
||||||
|
/// 如当前在登录页则执行完整登录,否则直接返回。
|
||||||
|
static func loginIfNeeded(
|
||||||
|
app: SuixinkanApp,
|
||||||
|
credentials: UITestCredentials,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) {
|
||||||
|
guard app.isOnLoginScreen else { return }
|
||||||
|
|
||||||
|
fillLoginForm(app: app, credentials: credentials)
|
||||||
|
submitLogin(app: app)
|
||||||
|
handleAccountSelectionIfNeeded(app: app, credentials: credentials)
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.isOnMainTabs,
|
||||||
|
"登录后应进入主 Tab。",
|
||||||
|
file: file,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 填写登录表单并勾选协议。
|
||||||
|
static func fillLoginForm(app: SuixinkanApp, credentials: UITestCredentials) {
|
||||||
|
let usernameField = app.application.textFields["login.username"]
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 8))
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.clearAndType(credentials.phone)
|
||||||
|
|
||||||
|
let passwordField = preferredPasswordField(in: app.application)
|
||||||
|
XCTAssertTrue(passwordField.waitForExistence(timeout: 4))
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.clearAndType(credentials.password)
|
||||||
|
|
||||||
|
let privacyToggle = app.application.buttons["login.privacy"]
|
||||||
|
if privacyToggle.exists {
|
||||||
|
privacyToggle.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 点击登录按钮。
|
||||||
|
static func submitLogin(app: SuixinkanApp) {
|
||||||
|
let submitButton = app.application.buttons["login.submit"]
|
||||||
|
XCTAssertTrue(submitButton.waitForExistence(timeout: 4))
|
||||||
|
submitButton.tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 v9 多账号选择页。
|
||||||
|
static func handleAccountSelectionIfNeeded(
|
||||||
|
app: SuixinkanApp,
|
||||||
|
credentials: UITestCredentials
|
||||||
|
) {
|
||||||
|
let accountSelectionTitle = app.application.navigationBars["选择账号"]
|
||||||
|
guard accountSelectionTitle.waitForExistence(timeout: 6) else { return }
|
||||||
|
|
||||||
|
if let accountName = credentials.accountName {
|
||||||
|
let accountCell = app.application.staticTexts[accountName]
|
||||||
|
if accountCell.waitForExistence(timeout: 4) {
|
||||||
|
accountCell.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmButton = app.application.buttons["进入系统"]
|
||||||
|
XCTAssertTrue(confirmButton.waitForExistence(timeout: 4))
|
||||||
|
confirmButton.tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 优先读取安全输入框,兼容调试构建下的普通文本框。
|
||||||
|
private static func preferredPasswordField(in app: XCUIApplication) -> XCUIElement {
|
||||||
|
let secureField = app.secureTextFields["login.password"]
|
||||||
|
if secureField.exists {
|
||||||
|
return secureField
|
||||||
|
}
|
||||||
|
return app.textFields["login.password"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension XCUIElement {
|
||||||
|
/// 清空并重新输入文本。
|
||||||
|
func clearAndType(_ text: String) {
|
||||||
|
guard let currentValue = value as? String else {
|
||||||
|
typeText(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tap()
|
||||||
|
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
|
||||||
|
typeText(deleteString)
|
||||||
|
typeText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
suixinkanUITests/Support/NavigationAssertions.swift
Normal file
74
suixinkanUITests/Support/NavigationAssertions.swift
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// NavigationAssertions.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 页面导航与可见性断言工具。
|
||||||
|
enum NavigationAssertions {
|
||||||
|
/// 等待导航栏标题出现。
|
||||||
|
@discardableResult
|
||||||
|
static func waitForNavigationTitle(
|
||||||
|
_ title: String,
|
||||||
|
in app: SuixinkanApp,
|
||||||
|
timeout: TimeInterval = 12,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) -> Bool {
|
||||||
|
let navigationBar = app.application.navigationBars[title]
|
||||||
|
let exists = navigationBar.waitForExistence(timeout: timeout)
|
||||||
|
XCTAssertTrue(exists, "未找到导航标题:\(title)", file: file, line: line)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 等待任意一个导航标题出现。
|
||||||
|
@discardableResult
|
||||||
|
static func waitForAnyNavigationTitle(
|
||||||
|
_ titles: [String],
|
||||||
|
in app: SuixinkanApp,
|
||||||
|
timeout: TimeInterval = 12,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) -> String? {
|
||||||
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
while Date() < deadline {
|
||||||
|
for title in titles {
|
||||||
|
if app.application.navigationBars[title].exists {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTFail("未找到任一导航标题:\(titles.joined(separator: " / "))", file: file, line: line)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 等待静态文案出现。
|
||||||
|
@discardableResult
|
||||||
|
static func waitForStaticText(
|
||||||
|
_ text: String,
|
||||||
|
in app: SuixinkanApp,
|
||||||
|
timeout: TimeInterval = 8,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) -> Bool {
|
||||||
|
let label = app.application.staticTexts[text]
|
||||||
|
let exists = label.waitForExistence(timeout: timeout)
|
||||||
|
XCTAssertTrue(exists, "未找到文案:\(text)", file: file, line: line)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 断言页面未停留在全局 Loading。
|
||||||
|
static func assertNotStuckOnLoading(
|
||||||
|
app: SuixinkanApp,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) {
|
||||||
|
let loading = app.application.staticTexts["加载中"]
|
||||||
|
XCTAssertFalse(loading.exists, "页面不应长期停留在全局 Loading。", file: file, line: line)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
suixinkanUITests/Support/SuixinkanUIApplication.swift
Normal file
78
suixinkanUITests/Support/SuixinkanUIApplication.swift
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// SuixinkanUIApplication.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 封装 XCUIApplication 的常用等待、权限弹窗和全局 Loading 处理。
|
||||||
|
final class SuixinkanApp {
|
||||||
|
let application: XCUIApplication
|
||||||
|
private let resetState: Bool
|
||||||
|
|
||||||
|
/// 创建并配置 UI Test 启动参数的应用实例。
|
||||||
|
init(resetState: Bool = false) {
|
||||||
|
self.resetState = resetState
|
||||||
|
application = XCUIApplication()
|
||||||
|
UITestLaunchConfiguration.apply(to: application, resetState: resetState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动 App 并等待首屏稳定。
|
||||||
|
@discardableResult
|
||||||
|
func launch(waitForLoading: Bool = true) -> Self {
|
||||||
|
if application.state == .runningForeground {
|
||||||
|
application.activate()
|
||||||
|
} else {
|
||||||
|
application.launch()
|
||||||
|
}
|
||||||
|
dismissSystemAlertsIfNeeded()
|
||||||
|
if waitForLoading {
|
||||||
|
waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 等待全局 Loading 层消失。
|
||||||
|
func waitForGlobalLoadingToDisappear(timeout: TimeInterval = 45) {
|
||||||
|
let loading = application.staticTexts["加载中"]
|
||||||
|
guard loading.waitForExistence(timeout: 2) else { return }
|
||||||
|
|
||||||
|
let predicate = NSPredicate(format: "exists == false")
|
||||||
|
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: loading)
|
||||||
|
_ = XCTWaiter.wait(for: [expectation], timeout: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理系统权限弹窗,尽量点允许以便页面继续加载。
|
||||||
|
func dismissSystemAlertsIfNeeded() {
|
||||||
|
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||||
|
let allowButtons = ["允许", "好", "使用App时允许", "Allow", "Allow While Using App", "OK"]
|
||||||
|
for title in allowButtons {
|
||||||
|
let button = springboard.buttons[title]
|
||||||
|
if button.waitForExistence(timeout: 1) {
|
||||||
|
button.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断当前是否仍在登录页。
|
||||||
|
var isOnLoginScreen: Bool {
|
||||||
|
application.staticTexts["login.title"].exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断主 Tab 是否已出现。
|
||||||
|
var isOnMainTabs: Bool {
|
||||||
|
application.buttons["首页"].waitForExistence(timeout: 1)
|
||||||
|
|| application.buttons["我的"].waitForExistence(timeout: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 等待冷启动后进入登录页或主 Tab。
|
||||||
|
func waitForInitialScreen(timeout: TimeInterval = 45) {
|
||||||
|
if application.staticTexts["login.title"].waitForExistence(timeout: 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = application.buttons["首页"].waitForExistence(timeout: timeout)
|
||||||
|
|| application.buttons["我的"].waitForExistence(timeout: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
suixinkanUITests/Support/TabBarNavigator.swift
Normal file
26
suixinkanUITests/Support/TabBarNavigator.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// TabBarNavigator.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 主 Tab 导航封装。
|
||||||
|
enum TabBarNavigator {
|
||||||
|
enum Tab: String, CaseIterable {
|
||||||
|
case home = "首页"
|
||||||
|
case orders = "订单"
|
||||||
|
case statistics = "数据"
|
||||||
|
case profile = "我的"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换到指定 Tab 并等待页面稳定。
|
||||||
|
static func select(_ tab: Tab, app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) {
|
||||||
|
let button = app.application.buttons[tab.rawValue]
|
||||||
|
XCTAssertTrue(button.waitForExistence(timeout: 8), "未找到 Tab:\(tab.rawValue)", file: file, line: line)
|
||||||
|
button.tap()
|
||||||
|
app.waitForGlobalLoadingToDisappear()
|
||||||
|
}
|
||||||
|
}
|
||||||
24
suixinkanUITests/Support/UITestAuthenticatedSession.swift
Normal file
24
suixinkanUITests/Support/UITestAuthenticatedSession.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// UITestAuthenticatedSession.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 已登录会话启动器,供需要主界面的 UI Test 复用。
|
||||||
|
enum UITestAuthenticatedSession {
|
||||||
|
/// 启动 App 并完成登录,返回配置好的应用包装器。
|
||||||
|
static func launchLoggedIn(
|
||||||
|
resetState: Bool = false,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) throws -> SuixinkanApp {
|
||||||
|
let credentials = try UITestLaunchConfiguration.requireCredentials(file: file, line: line)
|
||||||
|
let app = SuixinkanApp(resetState: resetState).launch()
|
||||||
|
app.waitForInitialScreen()
|
||||||
|
LoginFlow.loginIfNeeded(app: app, credentials: credentials, file: file, line: line)
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
}
|
||||||
43
suixinkanUITests/Support/UITestExpectationVerifier.swift
Normal file
43
suixinkanUITests/Support/UITestExpectationVerifier.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// UITestExpectationVerifier.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 根据首页菜单目录断言页面打开结果。
|
||||||
|
enum UITestExpectationVerifier {
|
||||||
|
/// 校验调试菜单打开后的目标页。
|
||||||
|
static func verify(
|
||||||
|
_ expectation: UITestHomeMenuCatalog.Expectation,
|
||||||
|
app: SuixinkanApp,
|
||||||
|
file: StaticString = #filePath,
|
||||||
|
line: UInt = #line
|
||||||
|
) {
|
||||||
|
switch expectation {
|
||||||
|
case .navigationTitle(let title):
|
||||||
|
NavigationAssertions.waitForNavigationTitle(title, in: app, file: file, line: line)
|
||||||
|
case .anyNavigationTitle(let titles):
|
||||||
|
_ = NavigationAssertions.waitForAnyNavigationTitle(titles, in: app, file: file, line: line)
|
||||||
|
case .tab(let tab):
|
||||||
|
switch tab {
|
||||||
|
case .orders:
|
||||||
|
NavigationAssertions.waitForNavigationTitle("订单", in: app, file: file, line: line)
|
||||||
|
case .statistics:
|
||||||
|
NavigationAssertions.waitForNavigationTitle("数据", in: app, file: file, line: line)
|
||||||
|
case .home:
|
||||||
|
XCTAssertTrue(app.application.staticTexts["常用应用"].waitForExistence(timeout: 8) || app.application.staticTexts["请选择景区"].exists, file: file, line: line)
|
||||||
|
case .profile:
|
||||||
|
NavigationAssertions.waitForNavigationTitle("个人信息", in: app, file: file, line: line)
|
||||||
|
}
|
||||||
|
case .staticText(let text):
|
||||||
|
NavigationAssertions.waitForStaticText(text, in: app, file: file, line: line)
|
||||||
|
case .unsupportedPlaceholder(let title):
|
||||||
|
_ = NavigationAssertions.waitForAnyNavigationTitle([title], in: app, file: file, line: line)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationAssertions.assertNotStuckOnLoading(app: app, file: file, line: line)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
suixinkanUITests/Support/UITestHomeMenuCatalog.swift
Normal file
73
suixinkanUITests/Support/UITestHomeMenuCatalog.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// UITestHomeMenuCatalog.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 首页调试菜单覆盖目录,按去重后的目标页组织。
|
||||||
|
enum UITestHomeMenuCatalog {
|
||||||
|
/// 打开菜单后的期望结果。
|
||||||
|
enum Expectation: Equatable {
|
||||||
|
case navigationTitle(String)
|
||||||
|
case anyNavigationTitle([String])
|
||||||
|
case tab(TabBarNavigator.Tab)
|
||||||
|
case staticText(String)
|
||||||
|
case unsupportedPlaceholder(title: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单个调试菜单用例。
|
||||||
|
struct Entry: Equatable {
|
||||||
|
let menuTitle: String
|
||||||
|
let expectation: Expectation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去重后的首页路由覆盖表,同义 URI 只保留一个菜单标题。
|
||||||
|
static let entries: [Entry] = [
|
||||||
|
Entry(menuTitle: "空间设置", expectation: .navigationTitle("个人信息")),
|
||||||
|
Entry(menuTitle: "相册管理", expectation: .navigationTitle("相册管理")),
|
||||||
|
Entry(menuTitle: "相册预览上传", expectation: .navigationTitle("相册预览上传")),
|
||||||
|
Entry(menuTitle: "我的钱包", expectation: .navigationTitle("我的钱包")),
|
||||||
|
Entry(menuTitle: "立即收款", expectation: .navigationTitle("立即收款")),
|
||||||
|
Entry(menuTitle: "相册云盘", expectation: .navigationTitle("相册云盘")),
|
||||||
|
Entry(menuTitle: "传输管理", expectation: .navigationTitle("传输记录")),
|
||||||
|
Entry(menuTitle: "素材管理", expectation: .navigationTitle("素材管理")),
|
||||||
|
Entry(menuTitle: "上传素材", expectation: .navigationTitle("上传素材")),
|
||||||
|
Entry(menuTitle: "任务管理", expectation: .navigationTitle("任务管理")),
|
||||||
|
Entry(menuTitle: "发布任务", expectation: .navigationTitle("发布任务")),
|
||||||
|
Entry(menuTitle: "日程管理", expectation: .navigationTitle("日程管理")),
|
||||||
|
Entry(menuTitle: "消息中心", expectation: .navigationTitle("消息中心")),
|
||||||
|
Entry(menuTitle: "打卡点管理", expectation: .navigationTitle("打卡点管理")),
|
||||||
|
Entry(menuTitle: "样片管理", expectation: .navigationTitle("样片管理")),
|
||||||
|
Entry(menuTitle: "上传样片", expectation: .navigationTitle("上传样片")),
|
||||||
|
Entry(menuTitle: "直播管理", expectation: .navigationTitle("直播管理")),
|
||||||
|
Entry(menuTitle: "直播相册", expectation: .navigationTitle("直播相册")),
|
||||||
|
Entry(menuTitle: "景区选择", expectation: .navigationTitle("景区选择")),
|
||||||
|
Entry(menuTitle: "景区申请", expectation: .navigationTitle("景区申请")),
|
||||||
|
Entry(menuTitle: "权限申请", expectation: .navigationTitle("申请权限")),
|
||||||
|
Entry(menuTitle: "权限申请状态", expectation: .anyNavigationTitle(["权限申请", "权限申请状态"])),
|
||||||
|
Entry(menuTitle: "景区结算", expectation: .navigationTitle("景区结算")),
|
||||||
|
Entry(menuTitle: "结算审核", expectation: .navigationTitle("结算审核")),
|
||||||
|
Entry(menuTitle: "核销订单", expectation: .tab(.orders)),
|
||||||
|
Entry(menuTitle: "景区订单", expectation: .tab(.orders)),
|
||||||
|
Entry(menuTitle: "数据统计", expectation: .tab(.statistics)),
|
||||||
|
Entry(menuTitle: "项目管理", expectation: .navigationTitle("项目管理")),
|
||||||
|
Entry(menuTitle: "店铺项目管理", expectation: .navigationTitle("店铺项目管理")),
|
||||||
|
Entry(menuTitle: "位置上报", expectation: .navigationTitle("位置上报")),
|
||||||
|
Entry(menuTitle: "注册邀请", expectation: .navigationTitle("摄影师邀请")),
|
||||||
|
Entry(menuTitle: "邀请记录", expectation: .navigationTitle("邀请记录")),
|
||||||
|
Entry(menuTitle: "店铺管理", expectation: .staticText("全部功能")),
|
||||||
|
Entry(menuTitle: "飞行管理", expectation: .unsupportedPlaceholder(title: "飞行管理")),
|
||||||
|
Entry(menuTitle: "飞手认证", expectation: .navigationTitle("飞手认证")),
|
||||||
|
Entry(menuTitle: "飞控", expectation: .unsupportedPlaceholder(title: "飞控")),
|
||||||
|
Entry(menuTitle: "排队管理", expectation: .navigationTitle("排队管理")),
|
||||||
|
Entry(menuTitle: "运营区域", expectation: .navigationTitle("运营区域")),
|
||||||
|
Entry(menuTitle: "押金订单详情", expectation: .navigationTitle("押金订单")),
|
||||||
|
Entry(menuTitle: "提现审核", expectation: .navigationTitle("提现审核")),
|
||||||
|
Entry(menuTitle: "定位上报历史", expectation: .navigationTitle("位置上报历史")),
|
||||||
|
Entry(menuTitle: "更多功能", expectation: .staticText("全部功能")),
|
||||||
|
Entry(menuTitle: "系统设置", expectation: .navigationTitle("设置"))
|
||||||
|
]
|
||||||
|
}
|
||||||
115
suixinkanUITests/Support/UITestLaunchConfiguration.swift
Normal file
115
suixinkanUITests/Support/UITestLaunchConfiguration.swift
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// UITestLaunchConfiguration.swift
|
||||||
|
// suixinkanUITests
|
||||||
|
//
|
||||||
|
// Created by Codex on 2026/6/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// UI Test 启动参数与环境变量,需与 App 侧 `AppUITestLaunchState` 保持同名。
|
||||||
|
enum UITestLaunchConfiguration {
|
||||||
|
static let uiTestsArgument = "-suixinkan-ui-tests"
|
||||||
|
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
||||||
|
|
||||||
|
static let phoneEnvironmentKey = "SUI_TEST_PHONE"
|
||||||
|
static let passwordEnvironmentKey = "SUI_TEST_PASSWORD"
|
||||||
|
static let accountNameEnvironmentKey = "SUI_TEST_ACCOUNT_NAME"
|
||||||
|
|
||||||
|
/// 为 XCUIApplication 注入 UI Test 启动参数;仅在需要冷启动空白态时传入 resetState。
|
||||||
|
static func apply(to app: XCUIApplication, resetState: Bool = false) {
|
||||||
|
if !app.launchArguments.contains(uiTestsArgument) {
|
||||||
|
app.launchArguments.append(uiTestsArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetState {
|
||||||
|
if !app.launchArguments.contains(resetArgument) {
|
||||||
|
app.launchArguments.append(resetArgument)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.launchArguments.removeAll { $0 == resetArgument }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 Scheme / xcodebuild 环境变量或本地 plist 读取测试账号。
|
||||||
|
static var credentials: UITestCredentials? {
|
||||||
|
if let environmentCredentials = credentialsFromEnvironment() {
|
||||||
|
return environmentCredentials
|
||||||
|
}
|
||||||
|
return credentialsFromLocalPlist()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缺少测试账号时跳过需要真实登录的用例。
|
||||||
|
static func requireCredentials(file: StaticString = #filePath, line: UInt = #line) throws -> UITestCredentials {
|
||||||
|
guard let credentials else {
|
||||||
|
throw XCTSkip(
|
||||||
|
"缺少 UI Test 账号。请配置环境变量 \(phoneEnvironmentKey) / \(passwordEnvironmentKey),或在本机创建 UITestCredentials.local.plist。"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 Scheme / xcodebuild 环境变量读取测试账号。
|
||||||
|
private static func credentialsFromEnvironment() -> UITestCredentials? {
|
||||||
|
let environment = ProcessInfo.processInfo.environment
|
||||||
|
guard
|
||||||
|
let phone = environment[phoneEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!phone.isEmpty,
|
||||||
|
let password = environment[passwordEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!password.isEmpty
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountName = environment[accountNameEnvironmentKey]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return UITestCredentials(
|
||||||
|
phone: phone,
|
||||||
|
password: password,
|
||||||
|
accountName: accountName?.isEmpty == false ? accountName : nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 gitignore 的本地 plist 读取测试账号,便于本机开发配置。
|
||||||
|
private static func credentialsFromLocalPlist() -> UITestCredentials? {
|
||||||
|
let plistURL = localCredentialsURL
|
||||||
|
guard
|
||||||
|
FileManager.default.fileExists(atPath: plistURL.path),
|
||||||
|
let dictionary = NSDictionary(contentsOf: plistURL) as? [String: String]
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let phone = dictionary[phoneEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!phone.isEmpty,
|
||||||
|
let password = dictionary[passwordEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!password.isEmpty
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountName = dictionary[accountNameEnvironmentKey]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return UITestCredentials(
|
||||||
|
phone: phone,
|
||||||
|
password: password,
|
||||||
|
accountName: accountName?.isEmpty == false ? accountName : nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地测试账号 plist 路径。
|
||||||
|
private static var localCredentialsURL: URL {
|
||||||
|
URL(fileURLWithPath: #filePath)
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.appendingPathComponent("UITestCredentials.local.plist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI Test 登录账号实体。
|
||||||
|
struct UITestCredentials: Equatable {
|
||||||
|
let phone: String
|
||||||
|
let password: String
|
||||||
|
let accountName: String?
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user