Compare commits

...

1 Commits

Author SHA1 Message Date
Peter Steinberger
03ade96729 WIP: Experimental local testing setup with test host app
- Created Xcode project for PeekabooTestHost app
- Added test files to explore running tests within app context
- Modified bundle ID in LocalOnlyTests to match test host
- Added various test runner implementations (XCTest, SimpleTestRunner)
- This approach aims to run tests within app bundle for proper permissions
2025-06-08 20:27:29 +01:00
17 changed files with 1679 additions and 81 deletions

View 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
import XCTest
class BasicTest: XCTestCase {
func testExample() {
XCTAssertTrue(true)
}
}

View File

@ -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")
}
}
}
}

View File

@ -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>

View 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
}
}

View 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)")
}
}
}

View File

@ -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)
]
)
]
)

View 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>

View 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()
}
}
}

View 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
}

View 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)")
}
}
}

View File

@ -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
}
}

View 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
}

View File

@ -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)