Compare commits
1 Commits
main
...
local-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ade96729 |
523
peekaboo-cli/PeekabooTestHost.xcodeproj/project.pbxproj
Normal file
523
peekaboo-cli/PeekabooTestHost.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,523 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
784E50882DF5FAE600E70CFB /* peekaboo in Frameworks */ = {isa = PBXBuildFile; productRef = 784E50872DF5FAE600E70CFB /* peekaboo */; };
|
||||
784E50982DF5FC6F00E70CFB /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 784E50972DF5FC6F00E70CFB /* XCTest.framework */; };
|
||||
784E50992DF5FC6F00E70CFB /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 784E50972DF5FC6F00E70CFB /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
785D645D2DF5F044000FB427 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 785D64462DF5F042000FB427 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 785D644D2DF5F042000FB427;
|
||||
remoteInfo = PeekabooTestHost;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
784E509A2DF5FC6F00E70CFB /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
784E50992DF5FC6F00E70CFB /* XCTest.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
784E50972DF5FC6F00E70CFB /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
|
||||
785D644E2DF5F042000FB427 /* PeekabooTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PeekabooTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
785D645C2DF5F044000FB427 /* PeekabooTestHostTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PeekabooTestHostTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
784E509E2DF5FD1200E70CFB /* Exceptions for "TestHost" folder in "PeekabooTestHost" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
BasicTest.swift,
|
||||
LocalOnlyTests.swift,
|
||||
PeekabooTestHostApp.swift,
|
||||
ScreenshotValidationTests.swift,
|
||||
TestTags.swift,
|
||||
);
|
||||
target = 785D644D2DF5F042000FB427 /* PeekabooTestHost */;
|
||||
};
|
||||
784E50A02DF5FD1400E70CFB /* Exceptions for "TestHost" folder in "PeekabooTestHostTests" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
BasicTest.swift,
|
||||
LocalOnlyTests.swift,
|
||||
PeekabooTestHostApp.swift,
|
||||
ScreenshotValidationTests.swift,
|
||||
TestTags.swift,
|
||||
);
|
||||
target = 785D645B2DF5F044000FB427 /* PeekabooTestHostTests */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
785D64502DF5F042000FB427 /* TestHost */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
784E509E2DF5FD1200E70CFB /* Exceptions for "TestHost" folder in "PeekabooTestHost" target */,
|
||||
784E50A02DF5FD1400E70CFB /* Exceptions for "TestHost" folder in "PeekabooTestHostTests" target */,
|
||||
);
|
||||
path = TestHost;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
785D644B2DF5F042000FB427 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
785D64592DF5F044000FB427 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
784E50882DF5FAE600E70CFB /* peekaboo in Frameworks */,
|
||||
784E50982DF5FC6F00E70CFB /* XCTest.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
7835A4972DF5F249001C9A47 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
784E50512DF5F28E00E70CFB /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
784E50972DF5FC6F00E70CFB /* XCTest.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
785D64452DF5F042000FB427 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7835A4972DF5F249001C9A47 /* Tests */,
|
||||
785D64502DF5F042000FB427 /* TestHost */,
|
||||
784E50512DF5F28E00E70CFB /* Frameworks */,
|
||||
785D644F2DF5F042000FB427 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
785D644F2DF5F042000FB427 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
785D644E2DF5F042000FB427 /* PeekabooTestHost.app */,
|
||||
785D645C2DF5F044000FB427 /* PeekabooTestHostTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
785D644D2DF5F042000FB427 /* PeekabooTestHost */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 785D64702DF5F045000FB427 /* Build configuration list for PBXNativeTarget "PeekabooTestHost" */;
|
||||
buildPhases = (
|
||||
785D644A2DF5F042000FB427 /* Sources */,
|
||||
785D644B2DF5F042000FB427 /* Frameworks */,
|
||||
785D644C2DF5F042000FB427 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
785D64502DF5F042000FB427 /* TestHost */,
|
||||
);
|
||||
name = PeekabooTestHost;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = PeekabooTestHost;
|
||||
productReference = 785D644E2DF5F042000FB427 /* PeekabooTestHost.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
785D645B2DF5F044000FB427 /* PeekabooTestHostTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 785D64732DF5F045000FB427 /* Build configuration list for PBXNativeTarget "PeekabooTestHostTests" */;
|
||||
buildPhases = (
|
||||
785D64582DF5F044000FB427 /* Sources */,
|
||||
785D64592DF5F044000FB427 /* Frameworks */,
|
||||
785D645A2DF5F044000FB427 /* Resources */,
|
||||
784E509A2DF5FC6F00E70CFB /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
785D645E2DF5F044000FB427 /* PBXTargetDependency */,
|
||||
);
|
||||
name = PeekabooTestHostTests;
|
||||
packageProductDependencies = (
|
||||
784E50872DF5FAE600E70CFB /* peekaboo */,
|
||||
);
|
||||
productName = PeekabooTestHostTests;
|
||||
productReference = 785D645C2DF5F044000FB427 /* PeekabooTestHostTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
785D64462DF5F042000FB427 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastUpgradeCheck = 1640;
|
||||
TargetAttributes = {
|
||||
785D644D2DF5F042000FB427 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
785D645B2DF5F044000FB427 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
TestTargetID = 785D644D2DF5F042000FB427;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 785D64492DF5F042000FB427 /* Build configuration list for PBXProject "PeekabooTestHost" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 785D64452DF5F042000FB427;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
784E50862DF5FAE600E70CFB /* XCLocalSwiftPackageReference "../peekaboo-cli" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 785D644F2DF5F042000FB427 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
785D644D2DF5F042000FB427 /* PeekabooTestHost */,
|
||||
785D645B2DF5F044000FB427 /* PeekabooTestHostTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
785D644C2DF5F042000FB427 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
785D645A2DF5F044000FB427 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
785D644A2DF5F042000FB427 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
785D64582DF5F044000FB427 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
785D645E2DF5F044000FB427 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 785D644D2DF5F042000FB427 /* PeekabooTestHost */;
|
||||
targetProxy = 785D645D2DF5F044000FB427 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
785D646E2DF5F045000FB427 /* 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;
|
||||
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 = 15.5;
|
||||
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;
|
||||
};
|
||||
785D646F2DF5F045000FB427 /* 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";
|
||||
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 = 15.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
785D64712DF5F045000FB427 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = TestHost/PeekabooTestHost.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/Frameworks";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.PeekabooTestHost;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
785D64722DF5F045000FB427 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = PeekabooTestHost/PeekabooTestHost.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/Frameworks";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.PeekabooTestHost;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
785D64742DF5F045000FB427 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/Frameworks";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.PeekabooTestHostTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PeekabooTestHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PeekabooTestHost";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
785D64752DF5F045000FB427 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/Frameworks";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.PeekabooTestHostTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PeekabooTestHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/PeekabooTestHost";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
785D64492DF5F042000FB427 /* Build configuration list for PBXProject "PeekabooTestHost" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
785D646E2DF5F045000FB427 /* Debug */,
|
||||
785D646F2DF5F045000FB427 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
785D64702DF5F045000FB427 /* Build configuration list for PBXNativeTarget "PeekabooTestHost" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
785D64712DF5F045000FB427 /* Debug */,
|
||||
785D64722DF5F045000FB427 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
785D64732DF5F045000FB427 /* Build configuration list for PBXNativeTarget "PeekabooTestHostTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
785D64742DF5F045000FB427 /* Debug */,
|
||||
785D64752DF5F045000FB427 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
784E50862DF5FAE600E70CFB /* XCLocalSwiftPackageReference "../peekaboo-cli" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = "../peekaboo-cli";
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
784E50872DF5FAE600E70CFB /* peekaboo */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = peekaboo;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 785D64462DF5F042000FB427 /* Project object */;
|
||||
}
|
||||
7
peekaboo-cli/PeekabooTestHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
peekaboo-cli/PeekabooTestHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D644D2DF5F042000FB427"
|
||||
BuildableName = "PeekabooTestHost.app"
|
||||
BlueprintName = "PeekabooTestHost"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D645B2DF5F044000FB427"
|
||||
BuildableName = "PeekabooTestHostTests.xctest"
|
||||
BlueprintName = "PeekabooTestHostTests"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D644D2DF5F042000FB427"
|
||||
BuildableName = "PeekabooTestHost.app"
|
||||
BlueprintName = "PeekabooTestHost"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUN_LOCAL_TESTS"
|
||||
value = "true"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D644D2DF5F042000FB427"
|
||||
BuildableName = "PeekabooTestHost.app"
|
||||
BlueprintName = "PeekabooTestHost"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D645B2DF5F044000FB427"
|
||||
BuildableName = "PeekabooTestHostTests.xctest"
|
||||
BlueprintName = "PeekabooTestHostTests"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "785D644D2DF5F042000FB427"
|
||||
BuildableName = "PeekabooTestHost.app"
|
||||
BlueprintName = "PeekabooTestHost"
|
||||
ReferencedContainer = "container:PeekabooTestHost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUN_LOCAL_TESTS"
|
||||
value = "true"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
peekaboo-cli/TestHost/BasicTest.swift
Normal file
7
peekaboo-cli/TestHost/BasicTest.swift
Normal file
@ -0,0 +1,7 @@
|
||||
import XCTest
|
||||
|
||||
class BasicTest: XCTestCase {
|
||||
func testExample() {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
@ -148,16 +148,25 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func runLocalTests() {
|
||||
testStatus = "Running tests..."
|
||||
addLog("Starting local test suite")
|
||||
|
||||
// This is where the Swift tests can interact with the host app
|
||||
// The tests can find this window by its identifier and perform actions
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
testStatus = "Tests can now interact with this window"
|
||||
addLog("Window is ready for test interactions")
|
||||
addLog("Run: swift test --enable-test-discovery --filter LocalIntegration")
|
||||
testStatus = "Running embedded tests..."
|
||||
addLog("Starting test execution from within app")
|
||||
|
||||
// Set environment for tests
|
||||
setenv("RUN_LOCAL_TESTS", "true", 1)
|
||||
|
||||
// Run the XCTest tests
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let testSuite = XCTestSuite(forTestCaseClass: LocalIntegrationTests.self)
|
||||
let testRun = testSuite.run()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.testStatus = "Tests completed: \(testRun.testCaseCount - testRun.failureCount)/\(testRun.testCaseCount) passed"
|
||||
self.addLog("Test execution finished")
|
||||
self.addLog("Total: \(testRun.testCaseCount) tests")
|
||||
self.addLog("Passed: \(testRun.testCaseCount - testRun.failureCount)")
|
||||
self.addLog("Failed: \(testRun.failureCount)")
|
||||
self.addLog("Duration: \(String(format: "%.2f", testRun.testDuration))s")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,26 +2,26 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>PeekabooTestHost</string>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.steipete.peekaboo.testhost</string>
|
||||
<string>com.steipete.PeekabooTestHost</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>PeekabooTestHost</string>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
</dict>
|
||||
|
||||
283
peekaboo-cli/TestHost/LocalOnlyTests.swift
Normal file
283
peekaboo-cli/TestHost/LocalOnlyTests.swift
Normal file
@ -0,0 +1,283 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
@testable import peekaboo
|
||||
import Testing
|
||||
|
||||
// MARK: - Local Only Tests
|
||||
|
||||
// These tests require the PeekabooTestHost app to be running and user interaction
|
||||
|
||||
@Suite(
|
||||
"Local Integration Tests",
|
||||
.tags(.integration, .localOnly),
|
||||
.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")
|
||||
)
|
||||
struct LocalIntegrationTests {
|
||||
// Test host app details
|
||||
static let testHostBundleId = "me.steipete.PeekabooTestHost"
|
||||
static let testHostAppName = "PeekabooTestHost"
|
||||
static let testWindowTitle = "Peekaboo Test Host"
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private func launchTestHost() async throws -> NSRunningApplication {
|
||||
// Check if test host is already running by name (SPM executables don't have bundle IDs)
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
if let existingApp = runningApps.first(where: {
|
||||
$0.localizedName == Self.testHostAppName ||
|
||||
$0.bundleIdentifier == Self.testHostBundleId
|
||||
}) {
|
||||
existingApp.activate()
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s
|
||||
return existingApp
|
||||
}
|
||||
|
||||
// Build and launch test host
|
||||
let testHostPath = try buildTestHost()
|
||||
|
||||
guard let url = URL(string: "file://\(testHostPath)") else {
|
||||
throw TestError.invalidPath(testHostPath)
|
||||
}
|
||||
|
||||
// Use modern NSWorkspace API
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
let app = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration)
|
||||
|
||||
// Wait for app to be ready
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1s
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
private func buildTestHost() throws -> String {
|
||||
// Build the test host app
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/swift")
|
||||
process.currentDirectoryURL = URL(fileURLWithPath: "/Users/steipete/Projects/Peekaboo/peekaboo-cli/TestHost")
|
||||
process.arguments = ["build", "-c", "debug"]
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw TestError.buildFailed
|
||||
}
|
||||
|
||||
return "/Users/steipete/Projects/Peekaboo/peekaboo-cli/TestHost/.build/debug/PeekabooTestHost"
|
||||
}
|
||||
|
||||
private func terminateTestHost() {
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
// Find by name or bundle ID (SPM executables don't have bundle IDs)
|
||||
if let app = runningApps.first(where: {
|
||||
$0.localizedName == Self.testHostAppName ||
|
||||
$0.bundleIdentifier == Self.testHostBundleId
|
||||
}) {
|
||||
app.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actual Screenshot Tests
|
||||
|
||||
@Test("Capture test host window screenshot", .tags(.screenshot))
|
||||
func captureTestHostWindow() async throws {
|
||||
_ = try await launchTestHost()
|
||||
defer { terminateTestHost() }
|
||||
|
||||
// Wait for window to be visible
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s
|
||||
|
||||
// Find the test host app
|
||||
let appInfo = try ApplicationFinder.findApplication(identifier: Self.testHostAppName)
|
||||
// SPM executables don't have bundle IDs, so accept either the expected ID or empty string
|
||||
#expect(appInfo.bundleIdentifier == Self.testHostBundleId || appInfo.bundleIdentifier?.isEmpty == true)
|
||||
|
||||
// Get windows for the app
|
||||
let windows = try WindowManager.getWindowsForApp(pid: appInfo.processIdentifier)
|
||||
#expect(!windows.isEmpty)
|
||||
|
||||
// Find our test window
|
||||
let testWindow = windows.first { $0.title.contains("Test Host") }
|
||||
#expect(testWindow != nil)
|
||||
|
||||
// Capture the window
|
||||
// In a real implementation, we would call the capture method
|
||||
// For now, we'll create a mock result
|
||||
let outputPath = "/tmp/peekaboo-test-window.png"
|
||||
|
||||
// Simulate capture by creating an empty file
|
||||
FileManager.default.createFile(atPath: outputPath, contents: nil)
|
||||
|
||||
let captureResult = ImageCaptureData(saved_files: [
|
||||
SavedFile(
|
||||
path: outputPath,
|
||||
item_label: "Test Window",
|
||||
window_title: testWindow?.title,
|
||||
window_id: UInt32(testWindow?.windowId ?? 0),
|
||||
window_index: 0,
|
||||
mime_type: "image/png"
|
||||
)
|
||||
])
|
||||
|
||||
#expect(captureResult.saved_files.count == 1)
|
||||
#expect(FileManager.default.fileExists(atPath: captureResult.saved_files[0].path))
|
||||
|
||||
// Verify the image
|
||||
if let image = NSImage(contentsOfFile: captureResult.saved_files[0].path) {
|
||||
#expect(image.size.width > 0)
|
||||
#expect(image.size.height > 0)
|
||||
} else {
|
||||
Issue.record("Failed to load captured image")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
try? FileManager.default.removeItem(atPath: captureResult.saved_files[0].path)
|
||||
}
|
||||
|
||||
@Test("Capture screen with test host visible", .tags(.screenshot))
|
||||
func captureScreenWithTestHost() async throws {
|
||||
let app = try await launchTestHost()
|
||||
defer { terminateTestHost() }
|
||||
|
||||
// Ensure test host is in foreground
|
||||
app.activate()
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s
|
||||
|
||||
// Capture the main screen
|
||||
let screens = NSScreen.screens
|
||||
#expect(!screens.isEmpty)
|
||||
|
||||
let mainScreen = screens[0]
|
||||
let displayId = mainScreen.displayID
|
||||
|
||||
// Simulate screen capture
|
||||
let outputPath = "/tmp/peekaboo-test-screen.png"
|
||||
FileManager.default.createFile(atPath: outputPath, contents: nil)
|
||||
|
||||
let captureResult = ImageCaptureData(saved_files: [
|
||||
SavedFile(
|
||||
path: outputPath,
|
||||
item_label: "Screen \(displayId)",
|
||||
window_title: nil,
|
||||
window_id: nil,
|
||||
window_index: nil,
|
||||
mime_type: "image/png"
|
||||
)
|
||||
])
|
||||
|
||||
#expect(captureResult.saved_files.count == 1)
|
||||
#expect(FileManager.default.fileExists(atPath: captureResult.saved_files[0].path))
|
||||
|
||||
// Clean up
|
||||
try? FileManager.default.removeItem(atPath: captureResult.saved_files[0].path)
|
||||
}
|
||||
|
||||
@Test("Test permission dialogs", .tags(.permissions))
|
||||
func permissionDialogs() async throws {
|
||||
_ = try await launchTestHost()
|
||||
defer { terminateTestHost() }
|
||||
|
||||
// Check current permissions
|
||||
let hasScreenRecording = PermissionsChecker.checkScreenRecordingPermission()
|
||||
let hasAccessibility = PermissionsChecker.checkAccessibilityPermission()
|
||||
|
||||
print("""
|
||||
Current permissions:
|
||||
- Screen Recording: \(hasScreenRecording)
|
||||
- Accessibility: \(hasAccessibility)
|
||||
|
||||
If permissions are not granted, the system will show dialogs when we try to use them.
|
||||
""")
|
||||
|
||||
// Try to trigger screen recording permission if not granted
|
||||
if !hasScreenRecording {
|
||||
print("Attempting to trigger screen recording permission dialog...")
|
||||
_ = CGWindowListCopyWindowInfo([.optionIncludingWindow], kCGNullWindowID)
|
||||
}
|
||||
|
||||
// Try to trigger accessibility permission if not granted
|
||||
if !hasAccessibility {
|
||||
print("Attempting to trigger accessibility permission dialog...")
|
||||
let options = ["AXTrustedCheckOptionPrompt": true]
|
||||
_ = AXIsProcessTrustedWithOptions(options as CFDictionary)
|
||||
}
|
||||
|
||||
// Give user time to interact with dialogs
|
||||
try await Task.sleep(nanoseconds: 2_000_000_000) // 2s
|
||||
|
||||
// Re-check permissions
|
||||
let newScreenRecording = PermissionsChecker.checkScreenRecordingPermission()
|
||||
let newAccessibility = PermissionsChecker.checkAccessibilityPermission()
|
||||
|
||||
print("""
|
||||
Updated permissions:
|
||||
- Screen Recording: \(hasScreenRecording) -> \(newScreenRecording)
|
||||
- Accessibility: \(hasAccessibility) -> \(newAccessibility)
|
||||
""")
|
||||
}
|
||||
|
||||
// MARK: - Multi-window capture tests
|
||||
|
||||
@Test("Capture multiple windows from test host", .tags(.screenshot, .multiWindow))
|
||||
func captureMultipleWindows() async throws {
|
||||
// This test would create multiple windows in the test host
|
||||
// and capture them individually
|
||||
let app = try await launchTestHost()
|
||||
defer { terminateTestHost() }
|
||||
|
||||
// Note: Future enhancement could add AppleScript to create multiple windows
|
||||
// Currently we verify we can enumerate windows
|
||||
|
||||
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
|
||||
print("Found \(windows.count) windows for test host")
|
||||
|
||||
for (index, window) in windows.enumerated() {
|
||||
print("Window \(index): \(window.title) (ID: \(window.windowId))")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Focus and foreground tests
|
||||
|
||||
@Test("Test foreground window capture", .tags(.screenshot, .focus))
|
||||
func foregroundCapture() async throws {
|
||||
let app = try await launchTestHost()
|
||||
defer { terminateTestHost() }
|
||||
|
||||
// Make sure test host is in foreground
|
||||
app.activate()
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s
|
||||
|
||||
// Capture with foreground focus
|
||||
_ = ImageCommand()
|
||||
// Set properties as needed
|
||||
// command.app = Self.testHostAppName
|
||||
|
||||
// This would test the actual foreground capture logic
|
||||
print("Test host should now be in foreground")
|
||||
#expect(app.isActive)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Error Types
|
||||
|
||||
enum TestError: Error {
|
||||
case buildFailed
|
||||
case invalidPath(String)
|
||||
case testHostNotFound
|
||||
case windowNotFound
|
||||
}
|
||||
|
||||
// Tags are defined in TestTags.swift
|
||||
|
||||
// MARK: - NSScreen Extension
|
||||
|
||||
extension NSScreen {
|
||||
var displayID: CGDirectDisplayID {
|
||||
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
||||
return deviceDescription[key] as? CGDirectDisplayID ?? 0
|
||||
}
|
||||
}
|
||||
113
peekaboo-cli/TestHost/LocalOnlyTestsXCTest.swift
Normal file
113
peekaboo-cli/TestHost/LocalOnlyTestsXCTest.swift
Normal file
@ -0,0 +1,113 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
@testable import peekaboo
|
||||
import XCTest
|
||||
|
||||
// MARK: - Local Only Tests for XCTest
|
||||
|
||||
class LocalIntegrationTests: XCTestCase {
|
||||
// Test host app details
|
||||
static let testHostBundleId = "me.steipete.PeekabooTestHost"
|
||||
static let testHostAppName = "PeekabooTestHost"
|
||||
static let testWindowTitle = "Peekaboo Test Host"
|
||||
|
||||
override class func setUp() {
|
||||
super.setUp()
|
||||
// Only run if environment variable is set
|
||||
guard ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true" else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actual Screenshot Tests
|
||||
|
||||
func testCaptureTestHostWindow() async throws {
|
||||
// Find the test host app (it should already be running - this IS the test host)
|
||||
let appInfo = try ApplicationFinder.findApplication(identifier: Self.testHostAppName)
|
||||
XCTAssertTrue(
|
||||
appInfo.bundleIdentifier == Self.testHostBundleId || appInfo.bundleIdentifier?.isEmpty == true,
|
||||
"Bundle ID should match or be empty for SPM executables"
|
||||
)
|
||||
|
||||
// Get windows for the app
|
||||
let windows = try WindowManager.getWindowsForApp(pid: appInfo.processIdentifier)
|
||||
print("Found \(windows.count) windows for test host")
|
||||
|
||||
// Find test window
|
||||
let testWindow = windows.first { $0.name?.contains(Self.testWindowTitle) ?? false }
|
||||
XCTAssertNotNil(testWindow, "Should find test host window")
|
||||
|
||||
guard let window = testWindow else { return }
|
||||
|
||||
// Capture the window
|
||||
let outputPath = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test_host_window.png")
|
||||
.path
|
||||
|
||||
let command = ImageCommand(
|
||||
mode: .window,
|
||||
path: outputPath,
|
||||
format: .png,
|
||||
app: Self.testHostAppName,
|
||||
windowIndex: 0,
|
||||
captureFocus: .background,
|
||||
jsonOutput: false
|
||||
)
|
||||
|
||||
do {
|
||||
let data = try await command.execute()
|
||||
XCTAssertFalse(data.saved_files.isEmpty, "Should save at least one file")
|
||||
|
||||
if let savedFile = data.saved_files.first {
|
||||
// Verify the file exists
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: savedFile.path))
|
||||
|
||||
// Load and verify the image
|
||||
if let image = NSImage(contentsOfFile: savedFile.path) {
|
||||
XCTAssertGreaterThan(image.size.width, 0)
|
||||
XCTAssertGreaterThan(image.size.height, 0)
|
||||
print("Successfully captured window: \(image.size)")
|
||||
} else {
|
||||
XCTFail("Failed to load captured image")
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try? FileManager.default.removeItem(atPath: savedFile.path)
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Screenshot capture failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testCaptureScreen() async throws {
|
||||
// Check permissions first
|
||||
let permissions = PermissionsChecker.checkPermissions()
|
||||
print("Current permissions:")
|
||||
print("- Screen Recording: \(permissions.screenRecording)")
|
||||
print("- Accessibility: \(permissions.accessibility)")
|
||||
|
||||
let outputPath = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test_screen.png")
|
||||
.path
|
||||
|
||||
let command = ImageCommand(
|
||||
mode: .screen,
|
||||
path: outputPath,
|
||||
format: .png,
|
||||
screenIndex: 0,
|
||||
jsonOutput: false
|
||||
)
|
||||
|
||||
do {
|
||||
let data = try await command.execute()
|
||||
XCTAssertFalse(data.saved_files.isEmpty)
|
||||
|
||||
if let savedFile = data.saved_files.first {
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: savedFile.path))
|
||||
try? FileManager.default.removeItem(atPath: savedFile.path)
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Screen capture failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "PeekabooTestHost",
|
||||
platforms: [
|
||||
.macOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.executable(
|
||||
name: "PeekabooTestHost",
|
||||
targets: ["PeekabooTestHost"]
|
||||
)
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "PeekabooTestHost",
|
||||
path: ".",
|
||||
sources: ["TestHostApp.swift", "ContentView.swift"],
|
||||
swiftSettings: [
|
||||
.swiftLanguageMode(.v6)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
10
peekaboo-cli/TestHost/PeekabooTestHost.entitlements
Executable file
10
peekaboo-cli/TestHost/PeekabooTestHost.entitlements
Executable file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
17
peekaboo-cli/TestHost/PeekabooTestHostApp.swift
Executable file
17
peekaboo-cli/TestHost/PeekabooTestHostApp.swift
Executable file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// PeekabooTestHostApp.swift
|
||||
// PeekabooTestHost
|
||||
//
|
||||
// Created by Peter Steinberger on 08.06.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct PeekabooTestHostApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
383
peekaboo-cli/TestHost/ScreenshotValidationTests.swift
Normal file
383
peekaboo-cli/TestHost/ScreenshotValidationTests.swift
Normal file
@ -0,0 +1,383 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
@testable import peekaboo
|
||||
import ScreenCaptureKit
|
||||
import Testing
|
||||
|
||||
@Suite(
|
||||
"Screenshot Validation Tests",
|
||||
.tags(.localOnly, .screenshot, .integration),
|
||||
.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")
|
||||
)
|
||||
struct ScreenshotValidationTests {
|
||||
// MARK: - Image Analysis Tests
|
||||
|
||||
@Test("Validate screenshot contains expected content", .tags(.imageAnalysis))
|
||||
@MainActor
|
||||
func validateScreenshotContent() async throws {
|
||||
// Create a temporary test window with known content
|
||||
let testWindow = createTestWindow(withContent: .text("PEEKABOO_TEST_12345"))
|
||||
defer { testWindow.close() }
|
||||
|
||||
// Give window time to render
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
|
||||
// Capture the window
|
||||
let windowID = CGWindowID(testWindow.windowNumber)
|
||||
|
||||
let outputPath = "/tmp/peekaboo-content-test.png"
|
||||
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
||||
|
||||
_ = try await captureWindowToFile(windowID: windowID, path: outputPath, format: .png)
|
||||
|
||||
// Load and analyze the image
|
||||
guard let image = NSImage(contentsOfFile: outputPath) else {
|
||||
Issue.record("Failed to load captured image")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify image properties
|
||||
#expect(image.size.width > 0)
|
||||
#expect(image.size.height > 0)
|
||||
|
||||
// In a real test, we could use OCR or pixel analysis to verify content
|
||||
print("Captured image size: \(image.size)")
|
||||
}
|
||||
|
||||
@Test("Compare screenshots for visual regression", .tags(.regression))
|
||||
@MainActor
|
||||
func visualRegressionTest() async throws {
|
||||
// Create test window with specific visual pattern
|
||||
let testWindow = createTestWindow(withContent: .grid)
|
||||
defer { testWindow.close() }
|
||||
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
|
||||
let windowID = CGWindowID(testWindow.windowNumber)
|
||||
|
||||
// Capture baseline
|
||||
let baselinePath = "/tmp/peekaboo-baseline.png"
|
||||
let currentPath = "/tmp/peekaboo-current.png"
|
||||
defer {
|
||||
try? FileManager.default.removeItem(atPath: baselinePath)
|
||||
try? FileManager.default.removeItem(atPath: currentPath)
|
||||
}
|
||||
|
||||
_ = try await captureWindowToFile(windowID: windowID, path: baselinePath, format: .png)
|
||||
|
||||
// Make a small change (in real tests, this would be application state change)
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
|
||||
// Capture current
|
||||
_ = try await captureWindowToFile(windowID: windowID, path: currentPath, format: .png)
|
||||
|
||||
// Compare images
|
||||
let baselineImage = NSImage(contentsOfFile: baselinePath)
|
||||
let currentImage = NSImage(contentsOfFile: currentPath)
|
||||
|
||||
#expect(baselineImage != nil)
|
||||
#expect(currentImage != nil)
|
||||
|
||||
// In practice, we'd use image diff algorithms here
|
||||
#expect(baselineImage!.size == currentImage!.size)
|
||||
}
|
||||
|
||||
@Test("Test different image formats", .tags(.formats))
|
||||
@MainActor
|
||||
func imageFormats() async throws {
|
||||
let testWindow = createTestWindow(withContent: .gradient)
|
||||
defer { testWindow.close() }
|
||||
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
|
||||
let windowID = CGWindowID(testWindow.windowNumber)
|
||||
|
||||
let formats: [ImageFormat] = [.png, .jpg]
|
||||
|
||||
for format in formats {
|
||||
let path = "/tmp/peekaboo-format-test.\(format.rawValue)"
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
_ = try await captureWindowToFile(windowID: windowID, path: path, format: format)
|
||||
|
||||
#expect(FileManager.default.fileExists(atPath: path))
|
||||
|
||||
// Verify file size makes sense for format
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
||||
let fileSize = attributes[.size] as? Int64 ?? 0
|
||||
|
||||
print("Format \(format.rawValue): \(fileSize) bytes")
|
||||
#expect(fileSize > 0)
|
||||
|
||||
// PNG should typically be larger than JPG for photos
|
||||
if format == .jpg {
|
||||
#expect(fileSize < 500_000) // JPG should be reasonably compressed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Multi-Display Tests
|
||||
|
||||
@Test("Capture from multiple displays", .tags(.multiDisplay))
|
||||
func multiDisplayCapture() async throws {
|
||||
let screens = NSScreen.screens
|
||||
print("Found \(screens.count) display(s)")
|
||||
|
||||
for (index, screen) in screens.enumerated() {
|
||||
let displayID = getDisplayID(for: screen)
|
||||
let outputPath = "/tmp/peekaboo-display-\(index).png"
|
||||
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
||||
|
||||
do {
|
||||
_ = try await captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
|
||||
|
||||
#expect(FileManager.default.fileExists(atPath: outputPath))
|
||||
|
||||
// Verify captured dimensions are reasonable
|
||||
if let image = NSImage(contentsOfFile: outputPath) {
|
||||
// The actual captured image dimensions depend on:
|
||||
// 1. The physical pixel dimensions of the display
|
||||
// 2. How macOS reports display information
|
||||
// 3. Whether the display is Retina or not
|
||||
//
|
||||
// Instead of trying to match exact dimensions, verify:
|
||||
// - The image has reasonable dimensions
|
||||
// - The aspect ratio is preserved
|
||||
|
||||
#expect(image.size.width > 0)
|
||||
#expect(image.size.height > 0)
|
||||
#expect(image.size.width <= 8192) // Max reasonable display width
|
||||
#expect(image.size.height <= 8192) // Max reasonable display height
|
||||
|
||||
// Verify aspect ratio is reasonable (between 1:3 and 3:1)
|
||||
let aspectRatio = image.size.width / image.size.height
|
||||
#expect(aspectRatio > 0.33)
|
||||
#expect(aspectRatio < 3.0)
|
||||
|
||||
print("Display \(index): captured \(image.size.width)x\(image.size.height)")
|
||||
}
|
||||
} catch {
|
||||
print("Failed to capture display \(index): \(error)")
|
||||
if screens.count == 1 {
|
||||
throw error // Re-throw if it's the only display
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Test("Screenshot capture performance", .tags(.performance))
|
||||
@MainActor
|
||||
func capturePerformance() async throws {
|
||||
let testWindow = createTestWindow(withContent: .solid(.white))
|
||||
defer { testWindow.close() }
|
||||
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
|
||||
let windowID = CGWindowID(testWindow.windowNumber)
|
||||
|
||||
let iterations = 10
|
||||
var captureTimes: [TimeInterval] = []
|
||||
|
||||
for iteration in 0..<iterations {
|
||||
let path = "/tmp/peekaboo-perf-\(iteration).png"
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
let start = CFAbsoluteTimeGetCurrent()
|
||||
_ = try await captureWindowToFile(windowID: windowID, path: path, format: .png)
|
||||
let duration = CFAbsoluteTimeGetCurrent() - start
|
||||
|
||||
captureTimes.append(duration)
|
||||
}
|
||||
|
||||
let averageTime = captureTimes.reduce(0, +) / Double(iterations)
|
||||
let maxTime = captureTimes.max() ?? 0
|
||||
|
||||
print("Capture performance: avg=\(averageTime * 1000)ms, max=\(maxTime * 1000)ms")
|
||||
|
||||
// Performance expectations
|
||||
// Note: Screen capture performance varies based on:
|
||||
// - Display resolution (4K/5K displays take longer)
|
||||
// - Number of displays
|
||||
// - System load
|
||||
// - Whether screen recording permission dialogs appear
|
||||
#expect(averageTime < 1.5) // Average should be under 1.5 seconds
|
||||
#expect(maxTime < 3.0) // Max should be under 3 seconds
|
||||
|
||||
// Performance benchmarks on typical hardware:
|
||||
// - Single 1080p display: ~100-200ms
|
||||
// - Single 4K display: ~300-500ms
|
||||
// - Multiple 4K displays: ~500-1500ms per capture
|
||||
// - First capture after permission grant: up to 3s
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
@MainActor
|
||||
private func createTestWindow(withContent content: TestContent) -> NSWindow {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 100, y: 100, width: 400, height: 300),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.title = "Peekaboo Test Window"
|
||||
window.isReleasedWhenClosed = false
|
||||
|
||||
let contentView = NSView(frame: window.contentRect(forFrameRect: window.frame))
|
||||
contentView.wantsLayer = true
|
||||
|
||||
switch content {
|
||||
case let .solid(color):
|
||||
contentView.layer?.backgroundColor = color.cgColor
|
||||
case .gradient:
|
||||
let gradient = CAGradientLayer()
|
||||
gradient.frame = contentView.bounds
|
||||
gradient.colors = [
|
||||
NSColor.red.cgColor,
|
||||
NSColor.yellow.cgColor,
|
||||
NSColor.green.cgColor,
|
||||
NSColor.blue.cgColor
|
||||
]
|
||||
contentView.layer?.addSublayer(gradient)
|
||||
case let .text(string):
|
||||
contentView.layer?.backgroundColor = NSColor.white.cgColor
|
||||
let textField = NSTextField(labelWithString: string)
|
||||
textField.font = NSFont.systemFont(ofSize: 24)
|
||||
textField.frame = contentView.bounds
|
||||
textField.alignment = .center
|
||||
contentView.addSubview(textField)
|
||||
case .grid:
|
||||
contentView.layer?.backgroundColor = NSColor.white.cgColor
|
||||
// Grid pattern would be drawn here
|
||||
}
|
||||
|
||||
window.contentView = contentView
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
private func captureWindowToFile(
|
||||
windowID: CGWindowID,
|
||||
path: String,
|
||||
format: ImageFormat
|
||||
) async throws -> ImageCaptureData {
|
||||
// Use modern ScreenCaptureKit API instead of deprecated CGWindowListCreateImage
|
||||
let image = try await captureWindowWithScreenCaptureKit(windowID: windowID)
|
||||
|
||||
// Save to file
|
||||
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
||||
try saveImage(nsImage, to: path, format: format)
|
||||
|
||||
return ImageCaptureData(saved_files: [
|
||||
SavedFile(
|
||||
path: path,
|
||||
item_label: "Window \(windowID)",
|
||||
window_title: nil,
|
||||
window_id: nil,
|
||||
window_index: nil,
|
||||
mime_type: format == .png ? "image/png" : "image/jpeg"
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
private func captureWindowWithScreenCaptureKit(windowID: CGWindowID) async throws -> CGImage {
|
||||
// Get available content
|
||||
let availableContent = try await SCShareableContent.current
|
||||
|
||||
// Find the window by ID
|
||||
guard let scWindow = availableContent.windows.first(where: { $0.windowID == windowID }) else {
|
||||
throw CaptureError.windowNotFound
|
||||
}
|
||||
|
||||
// Create content filter for the specific window
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
|
||||
// Configure capture settings
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.backgroundColor = .clear
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = false
|
||||
|
||||
// Capture the image
|
||||
return try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
private func captureDisplayToFile(
|
||||
displayID: CGDirectDisplayID,
|
||||
path: String,
|
||||
format: ImageFormat
|
||||
) async throws -> ImageCaptureData {
|
||||
let availableContent = try await SCShareableContent.current
|
||||
|
||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||
throw CaptureError.captureCreationFailed(nil)
|
||||
}
|
||||
|
||||
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
||||
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.backgroundColor = .clear
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = false
|
||||
|
||||
let image = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
||||
try saveImage(nsImage, to: path, format: format)
|
||||
|
||||
return ImageCaptureData(saved_files: [
|
||||
SavedFile(
|
||||
path: path,
|
||||
item_label: "Display \(displayID)",
|
||||
window_title: nil,
|
||||
window_id: nil,
|
||||
window_index: nil,
|
||||
mime_type: format == .png ? "image/png" : "image/jpeg"
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
private func saveImage(_ image: NSImage, to path: String, format: ImageFormat) throws {
|
||||
guard let tiffData = image.tiffRepresentation,
|
||||
let bitmap = NSBitmapImageRep(data: tiffData) else {
|
||||
throw CaptureError.fileWriteError(path, nil)
|
||||
}
|
||||
|
||||
let data: Data? = switch format {
|
||||
case .png:
|
||||
bitmap.representation(using: .png, properties: [:])
|
||||
case .jpg:
|
||||
bitmap.representation(using: .jpeg, properties: [.compressionFactor: 0.9])
|
||||
}
|
||||
|
||||
guard let imageData = data else {
|
||||
throw CaptureError.fileWriteError(path, nil)
|
||||
}
|
||||
|
||||
try imageData.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
|
||||
private func getDisplayID(for screen: NSScreen) -> CGDirectDisplayID {
|
||||
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
||||
return screen.deviceDescription[key] as? CGDirectDisplayID ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Content Types
|
||||
|
||||
enum TestContent {
|
||||
case solid(NSColor)
|
||||
case gradient
|
||||
case text(String)
|
||||
case grid
|
||||
}
|
||||
97
peekaboo-cli/TestHost/SimpleTestRunner.swift
Normal file
97
peekaboo-cli/TestHost/SimpleTestRunner.swift
Normal file
@ -0,0 +1,97 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
@testable import peekaboo
|
||||
|
||||
struct SimpleTestRunner {
|
||||
static func runPermissionTests(logger: @escaping (String) -> Void) async {
|
||||
logger("Starting permission tests...")
|
||||
|
||||
// Test 1: Check permissions
|
||||
await testPermissions(logger: logger)
|
||||
|
||||
// Test 2: Capture window
|
||||
await testWindowCapture(logger: logger)
|
||||
|
||||
// Test 3: Capture screen
|
||||
await testScreenCapture(logger: logger)
|
||||
|
||||
logger("All tests completed")
|
||||
}
|
||||
|
||||
private static func testPermissions(logger: @escaping (String) -> Void) async {
|
||||
logger("\n📋 Test: Check Permissions")
|
||||
let permissions = PermissionsChecker.checkPermissions()
|
||||
logger("Screen Recording: \(permissions.screenRecording ? "✅" : "❌")")
|
||||
logger("Accessibility: \(permissions.accessibility ? "✅" : "❌")")
|
||||
|
||||
if !permissions.screenRecording {
|
||||
logger("⚠️ Screen recording permission needed - dialogs should appear")
|
||||
}
|
||||
}
|
||||
|
||||
private static func testWindowCapture(logger: @escaping (String) -> Void) async {
|
||||
logger("\n📸 Test: Window Capture")
|
||||
|
||||
do {
|
||||
let appInfo = try ApplicationFinder.findApplication(identifier: "PeekabooTestHost")
|
||||
logger("Found app: \(appInfo.name)")
|
||||
|
||||
let windows = try WindowManager.getWindowsForApp(pid: appInfo.processIdentifier)
|
||||
logger("Found \(windows.count) windows")
|
||||
|
||||
if let window = windows.first {
|
||||
let outputPath = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test_window.png")
|
||||
.path
|
||||
|
||||
let command = ImageCommand(
|
||||
mode: .window,
|
||||
path: outputPath,
|
||||
format: .png,
|
||||
app: "PeekabooTestHost",
|
||||
windowIndex: 0,
|
||||
captureFocus: .background,
|
||||
jsonOutput: false
|
||||
)
|
||||
|
||||
let data = try await command.execute()
|
||||
logger("✅ Window captured: \(data.saved_files.count) files")
|
||||
|
||||
// Cleanup
|
||||
for file in data.saved_files {
|
||||
try? FileManager.default.removeItem(atPath: file.path)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger("❌ Window capture failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func testScreenCapture(logger: @escaping (String) -> Void) async {
|
||||
logger("\n🖥️ Test: Screen Capture")
|
||||
|
||||
do {
|
||||
let outputPath = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test_screen.png")
|
||||
.path
|
||||
|
||||
let command = ImageCommand(
|
||||
mode: .screen,
|
||||
path: outputPath,
|
||||
format: .png,
|
||||
screenIndex: 0,
|
||||
jsonOutput: false
|
||||
)
|
||||
|
||||
let data = try await command.execute()
|
||||
logger("✅ Screen captured: \(data.saved_files.count) files")
|
||||
|
||||
// Cleanup
|
||||
for file in data.saved_files {
|
||||
try? FileManager.default.removeItem(atPath: file.path)
|
||||
}
|
||||
} catch {
|
||||
logger("❌ Screen capture failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct TestHostApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.frame(minWidth: 600, minHeight: 500)
|
||||
.frame(width: 800, height: 600)
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
.windowStyle(.titleBar)
|
||||
.defaultSize(width: 800, height: 600)
|
||||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Make sure the app appears in foreground
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
// Set activation policy to regular app
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
28
peekaboo-cli/TestHost/TestTags.swift
Normal file
28
peekaboo-cli/TestHost/TestTags.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import Testing
|
||||
|
||||
extension Tag {
|
||||
@Tag static var fast: Self
|
||||
@Tag static var permissions: Self
|
||||
@Tag static var applicationFinder: Self
|
||||
@Tag static var windowManager: Self
|
||||
@Tag static var imageCapture: Self
|
||||
@Tag static var models: Self
|
||||
@Tag static var integration: Self
|
||||
@Tag static var unit: Self
|
||||
@Tag static var jsonOutput: Self
|
||||
@Tag static var logger: Self
|
||||
@Tag static var performance: Self
|
||||
@Tag static var concurrency: Self
|
||||
@Tag static var memory: Self
|
||||
@Tag static var browserFiltering: Self
|
||||
|
||||
// Local-only test tags
|
||||
@Tag static var localOnly: Self
|
||||
@Tag static var screenshot: Self
|
||||
@Tag static var multiWindow: Self
|
||||
@Tag static var focus: Self
|
||||
@Tag static var imageAnalysis: Self
|
||||
@Tag static var regression: Self
|
||||
@Tag static var formats: Self
|
||||
@Tag static var multiDisplay: Self
|
||||
}
|
||||
@ -14,16 +14,19 @@ import Testing
|
||||
)
|
||||
struct LocalIntegrationTests {
|
||||
// Test host app details
|
||||
static let testHostBundleId = "com.steipete.peekaboo.testhost"
|
||||
static let testHostBundleId = "me.steipete.PeekabooTestHost"
|
||||
static let testHostAppName = "PeekabooTestHost"
|
||||
static let testWindowTitle = "Peekaboo Test Host"
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private func launchTestHost() async throws -> NSRunningApplication {
|
||||
// Check if test host is already running
|
||||
// Check if test host is already running by name (SPM executables don't have bundle IDs)
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
if let existingApp = runningApps.first(where: { $0.bundleIdentifier == Self.testHostBundleId }) {
|
||||
if let existingApp = runningApps.first(where: {
|
||||
$0.localizedName == Self.testHostAppName ||
|
||||
$0.bundleIdentifier == Self.testHostBundleId
|
||||
}) {
|
||||
existingApp.activate()
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s
|
||||
return existingApp
|
||||
@ -69,7 +72,11 @@ struct LocalIntegrationTests {
|
||||
|
||||
private func terminateTestHost() {
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
if let app = runningApps.first(where: { $0.bundleIdentifier == Self.testHostBundleId }) {
|
||||
// Find by name or bundle ID (SPM executables don't have bundle IDs)
|
||||
if let app = runningApps.first(where: {
|
||||
$0.localizedName == Self.testHostAppName ||
|
||||
$0.bundleIdentifier == Self.testHostBundleId
|
||||
}) {
|
||||
app.terminate()
|
||||
}
|
||||
}
|
||||
@ -86,7 +93,8 @@ struct LocalIntegrationTests {
|
||||
|
||||
// Find the test host app
|
||||
let appInfo = try ApplicationFinder.findApplication(identifier: Self.testHostAppName)
|
||||
#expect(appInfo.bundleIdentifier == Self.testHostBundleId)
|
||||
// SPM executables don't have bundle IDs, so accept either the expected ID or empty string
|
||||
#expect(appInfo.bundleIdentifier == Self.testHostBundleId || appInfo.bundleIdentifier?.isEmpty == true)
|
||||
|
||||
// Get windows for the app
|
||||
let windows = try WindowManager.getWindowsForApp(pid: appInfo.processIdentifier)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user