import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); /** * Permission status for E2E tests requiring real automation. */ export interface E2EPermissionStatus { accessibility: boolean; screenRecording: boolean; platform: NodeJS.Platform; isCI: boolean; isHeadless: boolean; canRunRealAutomation: boolean; } /** * Result of permission check with actionable information. */ export interface PermissionCheckResult { canProceed: boolean; shouldSkip: boolean; skipReason?: string; status: E2EPermissionStatus; warnings: string[]; } /** * PermissionGuard for E2E tests. * * Checks macOS Accessibility and Screen Recording permissions * required for real native automation. Provides graceful skip * logic for CI environments or when permissions are unavailable. */ export class PermissionGuard { private cachedStatus: E2EPermissionStatus | null = null; /** * Check all permissions and determine if real automation tests can run. * * @returns PermissionCheckResult with status and skip reason if applicable */ async checkPermissions(): Promise { const status = await this.getPermissionStatus(); const warnings: string[] = []; // CI environments should always skip real automation tests if (status.isCI) { return { canProceed: false, shouldSkip: true, skipReason: 'Running in CI environment - real automation tests require a display', status, warnings: ['CI environment detected, skipping real automation tests'], }; } // Headless environments cannot run real automation if (status.isHeadless) { return { canProceed: false, shouldSkip: true, skipReason: 'Running in headless environment - real automation tests require a display', status, warnings: ['Headless environment detected, skipping real automation tests'], }; } // macOS-specific permission checks if (status.platform === 'darwin') { if (!status.accessibility) { warnings.push('macOS Accessibility permission not granted'); warnings.push('To grant: System Preferences > Security & Privacy > Privacy > Accessibility'); } if (!status.screenRecording) { warnings.push('macOS Screen Recording permission not granted'); warnings.push('To grant: System Preferences > Security & Privacy > Privacy > Screen Recording'); } if (!status.accessibility || !status.screenRecording) { return { canProceed: false, shouldSkip: true, skipReason: `Missing macOS permissions: ${[ !status.accessibility && 'Accessibility', !status.screenRecording && 'Screen Recording', ].filter(Boolean).join(', ')}`, status, warnings, }; } } // All checks passed return { canProceed: true, shouldSkip: false, status, warnings, }; } /** * Get the current permission status. */ async getPermissionStatus(): Promise { if (this.cachedStatus) { return this.cachedStatus; } const platform = process.platform; const isCI = this.detectCI(); const isHeadless = await this.detectHeadless(); let accessibility = true; let screenRecording = true; if (platform === 'darwin') { accessibility = await this.checkMacOSAccessibility(); screenRecording = await this.checkMacOSScreenRecording(); } const canRunRealAutomation = !isCI && !isHeadless && accessibility && screenRecording; this.cachedStatus = { accessibility, screenRecording, platform, isCI, isHeadless, canRunRealAutomation, }; return this.cachedStatus; } /** * Detect if running in a CI environment. */ private detectCI(): boolean { return !!( process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.JENKINS_URL || process.env.BUILDKITE || process.env.TF_BUILD // Azure DevOps ); } /** * Detect if running in a headless environment (no display). */ private async detectHeadless(): Promise { // Check for explicit headless environment variable if (process.env.HEADLESS === 'true' || process.env.DISPLAY === '') { return true; } // On Linux, check if DISPLAY is set if (process.platform === 'linux' && !process.env.DISPLAY) { return true; } // On macOS, check if we're in a non-GUI session if (process.platform === 'darwin') { try { // Check if we can access the WindowServer const { stdout } = await execAsync('pgrep -x WindowServer'); return !stdout.trim(); } catch { // pgrep returns non-zero if no process found return true; } } return false; } /** * Check macOS Accessibility permission without Electron. * Uses AppleScript to test if we can control system events. */ private async checkMacOSAccessibility(): Promise { try { // Try to use AppleScript to check accessibility // This will fail if accessibility permission is not granted await execAsync(`osascript -e 'tell application "System Events" to return name of first process'`); return true; } catch { // Permission denied or System Events not accessible return false; } } /** * Check macOS Screen Recording permission without Electron. * Uses `screencapture` heuristics to detect denial. */ private async checkMacOSScreenRecording(): Promise { try { const { stderr } = await execAsync('screencapture -x -c 2>&1 || true'); if (stderr.includes('permission') || stderr.includes('denied')) { return false; } return true; } catch { return false; } } /** * Clear cached permission status. */ clearCache(): void { this.cachedStatus = null; } /** * Format permission status for logging. */ formatStatus(status: E2EPermissionStatus): string { const lines = [ `Platform: ${status.platform}`, `CI Environment: ${status.isCI ? 'Yes' : 'No'}`, `Headless: ${status.isHeadless ? 'Yes' : 'No'}`, `Accessibility Permission: ${status.accessibility ? '✓' : '✗'}`, `Screen Recording Permission: ${status.screenRecording ? '✓' : '✗'}`, `Can Run Real Automation: ${status.canRunRealAutomation ? '✓' : '✗'}`, ]; return lines.join('\n'); } } /** * Singleton instance for use in tests. */ export const permissionGuard = new PermissionGuard(); /** * Skip helper for Cucumber tests. * Call in Before hook to skip tests if permissions are unavailable. * * @returns Skip reason if tests should be skipped, undefined otherwise */ export async function shouldSkipRealAutomationTests(): Promise { const result = await permissionGuard.checkPermissions(); if (result.shouldSkip) { console.warn('\n⚠️ Skipping real automation tests:'); console.warn(` ${result.skipReason}`); if (result.warnings.length > 0) { result.warnings.forEach(w => console.warn(` - ${w}`)); } return result.skipReason; } return undefined; } /** * Assert that real automation can proceed. * Throws an error with detailed information if not. */ export async function assertCanRunRealAutomation(): Promise { const result = await permissionGuard.checkPermissions(); if (!result.canProceed) { const status = permissionGuard.formatStatus(result.status); throw new Error( `Cannot run real automation tests:\n${result.skipReason}\n\nPermission Status:\n${status}` ); } }