You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							939 lines
						
					
					
						
							35 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							939 lines
						
					
					
						
							35 KiB
						
					
					
				| /** | |
|  * @fileoverview iOS test runner for Capacitor-based mobile app | |
|  *  | |
|  * This script handles the build and testing of the iOS app using Xcode's | |
|  * command-line tools. It ensures the app is properly synced with the latest | |
|  * web build and runs the test suite on a specified iOS simulator. | |
|  *  | |
|  * Process flow: | |
|  * 1. Clean and reset iOS platform (if needed) | |
|  * 2. Check prerequisites (Xcode, CocoaPods, Capacitor setup) | |
|  * 3. Sync Capacitor project with latest web build | |
|  * 4. Build app for iOS simulator | |
|  * 5. Run XCTest suite | |
|  *  | |
|  * Prerequisites: | |
|  * - macOS operating system | |
|  * - Xcode installed with command line tools | |
|  * - iOS simulator available | |
|  * - Capacitor iOS platform added to project | |
|  * - Valid iOS development certificates | |
|  *  | |
|  * Exit codes: | |
|  * - 0: Tests completed successfully | |
|  * - 1: Build or test failure | |
|  *  | |
|  * @example | |
|  * // Run directly | |
|  * node scripts/test-ios.js | |
|  *  | |
|  * // Run via npm script | |
|  * npm run test:ios | |
|  *  | |
|  * @requires child_process | |
|  * @requires path | |
|  * @requires fs | |
|  *  | |
|  * @author TimeSafari Team | |
|  * @license MIT | |
|  */ | |
| 
 | |
| const { execSync } = require('child_process'); | |
| const { join } = require('path'); | |
| const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync, statSync, accessSync } = require('fs'); | |
| const readline = require('readline'); | |
| const rl = readline.createInterface({ | |
|     input: process.stdin, | |
|     output: process.stdout | |
| }); | |
| const { constants } = require('fs'); | |
| 
 | |
| const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve)); | |
| 
 | |
| // Make sure to close readline at the end | |
| process.on('SIGINT', () => { | |
|     rl.close(); | |
|     process.exit(); | |
| }); | |
| 
 | |
| // Format date as YYYY-MM-DD-HHMMSS | |
| const getLogFileName = () => { | |
|     const now = new Date(); | |
|     const date = now.toISOString().split('T')[0]; | |
|     const time = now.toTimeString().split(' ')[0].replace(/:/g, ''); | |
|     return `build_logs/ios-build-${date}-${time}.log`; | |
| }; | |
| 
 | |
| // Create logger function | |
| const createLogger = (logFile) => { | |
|     return (message) => { | |
|         const timestamp = new Date().toISOString(); | |
|         const logMessage = `[${timestamp}] ${message}\n`; | |
|         console.log(message); | |
|         appendFileSync(logFile, logMessage); | |
|     }; | |
| }; | |
| 
 | |
| /** | |
|  * Clean up and reset iOS platform | |
|  * This function completely removes and recreates the iOS platform to ensure a fresh setup | |
|  * @param {function} log - Logging function | |
|  * @returns {boolean} - Success status | |
|  */ | |
| const cleanIosPlatform = async (log) => { | |
|     log('🧹 Cleaning iOS platform (complete reset)...'); | |
|      | |
|     // Check for package.json and capacitor.config.ts/js | |
|     if (!existsSync('package.json')) { | |
|         log('⚠️ package.json not found. Are you in the correct directory?'); | |
|         throw new Error('package.json not found. Cannot continue without project configuration.'); | |
|     } | |
|     log('✅ package.json exists'); | |
|      | |
|     const capacitorConfigExists =  | |
|         existsSync('capacitor.config.ts') ||  | |
|         existsSync('capacitor.config.js') ||  | |
|         existsSync('capacitor.config.json'); | |
|      | |
|     if (!capacitorConfigExists) { | |
|         log('⚠️ Capacitor config file not found'); | |
|         log('Creating minimal capacitor.config.ts...'); | |
|          | |
|         try { | |
|             // Get app name from package.json | |
|             const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); | |
|             const appName = packageJson.name || 'App'; | |
|             const appId = packageJson.build.appId || 'io.ionic.starter'; | |
|              | |
|             // Create a minimal capacitor config | |
|             const capacitorConfig = ` | |
| import { CapacitorConfig } from '@capacitor/cli'; | |
|  | |
| const config: CapacitorConfig = { | |
|   appId: '${appId}', | |
|   appName: '${appName}', | |
|   webDir: 'dist', | |
|   bundledWebRuntime: false | |
| }; | |
|  | |
| export default config; | |
|             `.trim(); | |
|              | |
|             writeFileSync('capacitor.config.ts', capacitorConfig); | |
|             log('✅ Created capacitor.config.ts'); | |
|         } catch (configError) { | |
|             log('⚠️ Failed to create Capacitor config file'); | |
|             log('Please create a capacitor.config.ts file manually'); | |
|             throw new Error('Capacitor configuration missing. Please configure manually.'); | |
|         } | |
|     } else { | |
|         log('✅ Capacitor config exists'); | |
|     } | |
|      | |
|     // Check if the platform exists first | |
|     if (existsSync('ios')) { | |
|         log('🗑️ Removing existing iOS platform directory...'); | |
|         try { | |
|             execSync('rm -rf ios', { stdio: 'inherit' }); | |
|             log('✅ Existing iOS platform removed'); | |
|         } catch (error) { | |
|             log(`⚠️ Error removing iOS platform: ${error.message}`); | |
|             log('⚠️ You may need to manually remove the ios directory'); | |
|             return false; | |
|         } | |
|     } | |
|      | |
|     // Rebuild web assets first to ensure they're available | |
|     log('🔄 Building web assets before adding iOS platform...'); | |
|     try { | |
|         execSync('rm -rf dist', { stdio: 'inherit' }); | |
|         execSync('npm run build:web', { stdio: 'inherit' }); | |
|         execSync('npm run build:capacitor', { stdio: 'inherit' }); | |
|         log('✅ Web assets built successfully'); | |
|     } catch (error) { | |
|         log(`⚠️ Error building web assets: ${error.message}`); | |
|         log('⚠️ Continuing with platform addition, but it may fail if web assets are required'); | |
|     } | |
|      | |
|     // Add the platform back | |
|     log('➕ Adding iOS platform...'); | |
|     try { | |
|         execSync('npx cap add ios', { stdio: 'inherit' }); | |
|         log('✅ iOS platform added successfully'); | |
|          | |
|         // Verify critical files were created | |
|         if (!existsSync('ios/App/Podfile')) { | |
|             log('⚠️ Podfile was not created - something is wrong with the Capacitor setup'); | |
|             return false; | |
|         } | |
|          | |
|         if (!existsSync('ios/App/App/Info.plist')) { | |
|             log('⚠️ Info.plist was not created - something is wrong with the Capacitor setup'); | |
|             return false; | |
|         } | |
|          | |
|         log('✅ iOS platform setup verified - critical files exist'); | |
|         return true; | |
|     } catch (error) { | |
|         log(`⚠️ Error adding iOS platform: ${error.message}`); | |
|         return false; | |
|     } | |
| }; | |
| 
 | |
| /** | |
|  * Check all prerequisites for iOS testing | |
|  * Verifies and attempts to install/initialize all required components | |
|  */ | |
| const checkPrerequisites = async (log) => { | |
|     log('🔍 Checking prerequisites for iOS testing...'); | |
|      | |
|     // Check for macOS | |
|     if (process.platform !== 'darwin') { | |
|         throw new Error('iOS testing is only supported on macOS'); | |
|     } | |
|     log('✅ Running on macOS'); | |
|      | |
|     // Verify Xcode installation | |
|     try { | |
|         const xcodeOutput = execSync('xcode-select -p').toString().trim(); | |
|         log(`✅ Xcode command line tools found at: ${xcodeOutput}`); | |
|     } catch (error) { | |
|         log('⚠️ Xcode command line tools not found'); | |
|         log('Please install Xcode from the App Store and run:'); | |
|         log('xcode-select --install'); | |
|         throw new Error('Xcode command line tools not found. Please install Xcode first.'); | |
|     } | |
|      | |
|     // Check Xcode version | |
|     try { | |
|         const xcodeVersionOutput = execSync('xcodebuild -version').toString().trim(); | |
|         log(`✅ Xcode version: ${xcodeVersionOutput.split('\n')[0]}`); | |
|     } catch (error) { | |
|         log('⚠️ Unable to determine Xcode version'); | |
|     } | |
|      | |
|     // Check for CocoaPods | |
|     try { | |
|         const podVersionOutput = execSync('pod --version').toString().trim(); | |
|         log(`✅ CocoaPods version: ${podVersionOutput}`); | |
|     } catch (error) { | |
|         log('⚠️ CocoaPods not found'); | |
|         log('Attempting to install CocoaPods...'); | |
|          | |
|         try { | |
|             log('🔄 Installing CocoaPods via gem...'); | |
|             execSync('gem install cocoapods', { stdio: 'inherit' }); | |
|             log('✅ CocoaPods installed successfully'); | |
|         } catch (gemError) { | |
|             log('⚠️ Failed to install CocoaPods via gem'); | |
|             log('Please install CocoaPods manually:'); | |
|             log('1. sudo gem install cocoapods'); | |
|             log('2. brew install cocoapods'); | |
|             throw new Error('CocoaPods installation failed. Please install manually.'); | |
|         } | |
|     } | |
|      | |
|     log('✅ All prerequisites for iOS testing are met'); | |
|     return true; | |
| }; | |
| 
 | |
| // Check for iOS simulator | |
| const checkSimulator = async (log) => { | |
|     log('🔍 Checking for iOS simulator...'); | |
|     const simulatorsOutput = execSync('xcrun simctl list devices available -j').toString(); | |
|     const simulatorsData = JSON.parse(simulatorsOutput); | |
|      | |
|     // Get all available devices/simulators with their UDIDs | |
|     const allDevices = []; | |
|      | |
|     // Process all runtime groups (iOS versions) | |
|     Object.entries(simulatorsData.devices).forEach(([runtime, devices]) => { | |
|         devices.forEach(device => { | |
|             allDevices.push({ | |
|                 name: device.name, | |
|                 udid: device.udid, | |
|                 state: device.state, | |
|                 runtime: runtime, | |
|                 isIphone: device.name.includes('iPhone'), | |
|             }); | |
|         }); | |
|     }); | |
|      | |
|     // Check for booted simulators first | |
|     const bootedDevices = allDevices.filter(device => device.state === 'Booted'); | |
|      | |
|     if (bootedDevices.length > 0) { | |
|         log(`📱 Found ${bootedDevices.length} running simulator(s): ${bootedDevices.map(d => d.name).join(', ')}`); | |
|         return bootedDevices; | |
|     } | |
|      | |
|     // No booted devices found, try to boot one | |
|     log('⚠️ No running iOS simulator found. Attempting to boot one...'); | |
|      | |
|     // Prefer iPhone devices, especially newer models | |
|     const preferredDevices = [ | |
|         'iPhone 15', 'iPhone 14', 'iPhone 13', 'iPhone 12', 'iPhone',  // Prefer newer iPhones first | |
|         'iPad'  // Then iPads if no iPhones available | |
|     ]; | |
|      | |
|     let deviceToLaunch = null; | |
|      | |
|     // Try to find a device from our preferred list | |
|     for (const preferredName of preferredDevices) { | |
|         const matchingDevices = allDevices.filter(device =>  | |
|             device.name.includes(preferredName) && device.state === 'Shutdown'); | |
|          | |
|         if (matchingDevices.length > 0) { | |
|             // Sort by runtime to prefer newer iOS versions | |
|             matchingDevices.sort((a, b) => b.runtime.localeCompare(a.runtime)); | |
|             deviceToLaunch = matchingDevices[0]; | |
|             break; | |
|         } | |
|     } | |
|      | |
|     // If no preferred device found, take any available device | |
|     if (!deviceToLaunch && allDevices.length > 0) { | |
|         const availableDevices = allDevices.filter(device => device.state === 'Shutdown'); | |
|         if (availableDevices.length > 0) { | |
|             deviceToLaunch = availableDevices[0]; | |
|         } | |
|     } | |
|      | |
|     if (!deviceToLaunch) { | |
|         throw new Error('No available iOS simulators found. Please create a simulator in Xcode first.'); | |
|     } | |
|      | |
|     // Boot the selected simulator | |
|     log(`🚀 Booting iOS simulator: ${deviceToLaunch.name} (${deviceToLaunch.runtime})`); | |
|     execSync(`xcrun simctl boot ${deviceToLaunch.udid}`); | |
|      | |
|     // Wait for simulator to fully boot | |
|     log('⏳ Waiting for simulator to boot completely...'); | |
|     // Give the simulator time to fully boot before proceeding | |
|     await new Promise(resolve => setTimeout(resolve, 10000)); | |
|      | |
|     log(`✅ Successfully booted simulator: ${deviceToLaunch.name}`); | |
|      | |
|     return [{ name: deviceToLaunch.name, udid: deviceToLaunch.udid }]; | |
| }; | |
| 
 | |
| // Verify Xcode installation | |
| const verifyXcodeInstallation = (log) => { | |
|     log('🔍 Checking Xcode installation...'); | |
|     try { | |
|         execSync('xcode-select -p'); | |
|         log('✅ Xcode command line tools found'); | |
|     } catch (error) { | |
|         throw new Error('Xcode command line tools not found. Please install Xcode first.'); | |
|     } | |
| }; | |
| 
 | |
| // Generate test data using generate_data.ts | |
| const generateTestData = async (log) => { | |
|     log('\n🔍 Starting test data generation...'); | |
|      | |
|     // Check directory structure | |
|     log('📁 Current directory:', process.cwd()); | |
|     log('📁 Directory contents:', require('fs').readdirSync('.')); | |
|      | |
|     if (!existsSync('.generated')) { | |
|         log('📁 Creating .generated directory'); | |
|         mkdirSync('.generated', { recursive: true }); | |
|     } | |
| 
 | |
|     try { | |
|         log('🔄 Attempting to run generate_data.ts...'); | |
|         execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); | |
|         log('✅ Test data generation completed'); | |
|          | |
|         // Verify and log generated files content | |
|         const requiredFiles = [ | |
|             '.generated/test-env.json', | |
|             '.generated/claim_details.json', | |
|             '.generated/contacts.json' | |
|         ]; | |
|          | |
|         log('\n📝 Verifying generated files:'); | |
|         for (const file of requiredFiles) { | |
|             if (!existsSync(file)) { | |
|                 log(`❌ Missing file: ${file}`); | |
|             } else { | |
|                 const content = readFileSync(file, 'utf8'); | |
|                 log(`\n📄 Content of ${file}:`); | |
|                 log(content); | |
|                 try { | |
|                     const parsed = JSON.parse(content); | |
|                     if (file.includes('test-env.json')) { | |
|                         log('🔑 CONTACT1_DID in test-env:', parsed.CONTACT1_DID); | |
|                     } | |
|                     if (file.includes('contacts.json')) { | |
|                         log('👥 First contact DID:', parsed[0]?.did); | |
|                     } | |
|                 } catch (e) { | |
|                     log(`❌ Error parsing ${file}:`, e); | |
|                 } | |
|             } | |
|         } | |
|     } catch (error) { | |
|         log(`\n⚠️ Test data generation failed: ${error.message}`); | |
|         log('⚠️ Creating fallback test data...'); | |
|          | |
|         // Create fallback data with detailed logging | |
|         const fallbackTestEnv = { | |
|             "CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B", | |
|             "APP_URL": "https://app.timesafari.example" | |
|         }; | |
|          | |
|         const fallbackContacts = [ | |
|             { | |
|                 "id": "contact1", | |
|                 "name": "Test Contact", | |
|                 "did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B" | |
|             } | |
|         ]; | |
|          | |
|         log('\n📝 Writing fallback data:'); | |
|         log('TestEnv:', JSON.stringify(fallbackTestEnv, null, 2)); | |
|         log('Contacts:', JSON.stringify(fallbackContacts, null, 2)); | |
|          | |
|         writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2)); | |
|         writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2)); | |
|          | |
|         // Verify fallback data was written | |
|         log('\n🔍 Verifying fallback data:'); | |
|         try { | |
|             const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8')); | |
|             const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8')); | |
|             log('Written TestEnv:', writtenTestEnv); | |
|             log('Written Contacts:', writtenContacts); | |
|         } catch (e) { | |
|             log('❌ Error verifying fallback data:', e); | |
|         } | |
|     } | |
| }; | |
| 
 | |
| // Build web assets | |
| const buildWebAssets = async (log) => { | |
|     log('🌐 Building web assets...'); | |
|     execSync('rm -rf dist', { stdio: 'inherit' }); | |
|     execSync('npm run build:web', { stdio: 'inherit' }); | |
|     execSync('npm run build:capacitor', { stdio: 'inherit' }); | |
|     log('✅ Web assets built successfully'); | |
| }; | |
| 
 | |
| // Configure iOS project | |
| const configureIosProject = async (log) => { | |
|     log('📱 Configuring iOS project...'); | |
|      | |
|     // Skip cap sync since we just did a clean platform add | |
|     log('✅ Using freshly created iOS platform'); | |
| 
 | |
|     // Register URL scheme for deeplink tests | |
|     log('🔗 Configuring URL scheme for deeplink tests...'); | |
|     if (checkAndRegisterUrlScheme(log)) { | |
|         log('✅ URL scheme configuration completed'); | |
|     } else { | |
|         log('⚠️ URL scheme could not be registered automatically'); | |
|         log('⚠️ Deeplink tests may not work correctly'); | |
|     } | |
| 
 | |
|     log('⚙️ Installing CocoaPods dependencies...'); | |
|     try { | |
|         // Try to run pod install normally first | |
|         log('🔄 Running "pod install" in ios/App directory...'); | |
|     execSync('cd ios/App && pod install', { stdio: 'inherit' }); | |
|     log('✅ CocoaPods installation completed'); | |
|     } catch (error) { | |
|         // If that fails, provide detailed instructions | |
|         log(`⚠️ CocoaPods installation failed: ${error.message}`); | |
|         log('⚠️ Please ensure CocoaPods is installed correctly:'); | |
|         log('1. If using system Ruby: "sudo gem install cocoapods"'); | |
|         log('2. If using Homebrew Ruby: "brew install cocoapods"'); | |
|         log('3. Then run: "cd ios/App && pod install"'); | |
|          | |
|         // Try to continue despite the error | |
|         log('⚠️ Attempting to continue with the build process...'); | |
|     } | |
|      | |
|     // Add information about iOS security dialogs | |
|     log('\n📱 iOS Security Dialog Information:'); | |
|     log('⚠️ iOS will display security confirmation dialogs when testing deeplinks'); | |
|     log('⚠️ This is a security feature of iOS and cannot be bypassed in normal testing'); | |
|     log('⚠️ You will need to manually approve each deeplink test by clicking "Open" in the dialog'); | |
|     log('⚠️ The app must be running in the foreground for deeplinks to work properly'); | |
|     log('⚠️ If tests appear to hang, check if a security dialog is waiting for your confirmation'); | |
| }; | |
| 
 | |
| // Build and test iOS project | |
| const buildAndTestIos = async (log, simulator) => { | |
|     const simulatorName = simulator[0].name; | |
|     log('🏗️ Building iOS project...', simulator[0]); | |
|     execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' }); | |
|     log('✅ Xcode clean completed'); | |
|      | |
|     log(`🏗️ Building for simulator: ${simulatorName}`); | |
|     execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,OS=17.2,name=${simulatorName}"`, { stdio: 'inherit' }); | |
|     log('✅ Xcode build completed'); | |
| 
 | |
|     // Check if the project is configured for testing by querying the scheme capabilities | |
|     try { | |
|         log(`🧪 Checking if scheme is configured for testing`); | |
|         const schemeInfo = execSync(`cd ios/App && xcodebuild -scheme App -showBuildSettings | grep TEST`).toString(); | |
|          | |
|         if (schemeInfo.includes('ENABLE_TESTABILITY = YES')) { | |
|             log(`🧪 Attempting to run tests on simulator: ${simulatorName}`); | |
|             try { | |
|                 execSync(`cd ios/App && xcodebuild test -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=${simulatorName}"`, { stdio: 'inherit' }); | |
|                 log('✅ iOS tests completed successfully'); | |
|             } catch (testError) { | |
|                 log(`⚠️ Tests failed or scheme not properly configured for testing: ${testError.message}`); | |
|                 log('⚠️ This is normal if no test targets have been added to the project'); | |
|                 log('⚠️ Skipping test step and continuing with the app launch'); | |
|             } | |
|         } else { | |
|             log('⚠️ Project does not have testing enabled in build settings'); | |
|             log('⚠️ Skipping test step and continuing with the app launch'); | |
|         } | |
|     } catch (error) { | |
|         log('⚠️ Unable to determine if testing is configured'); | |
|         log('⚠️ Skipping test step and continuing with the app launch'); | |
|     } | |
| }; | |
| 
 | |
| // Run the app | |
| const runIosApp = async (log, simulator) => { | |
|     const simulatorName = simulator[0].name; | |
|     const simulatorUdid = simulator[0].udid; | |
|      | |
|     log(`📱 Running app in simulator: ${simulatorName} (${simulatorUdid})...`); | |
|     // Use the --target parameter to specify the device directly, avoiding the UI prompt | |
|     execSync(`npx cap run ios --target="${simulatorUdid}"`, { stdio: 'inherit' }); | |
|     log('✅ App launched successfully'); | |
| }; | |
| 
 | |
| const validateTestData = (log) => { | |
|     log('\n=== VALIDATING TEST DATA ==='); | |
|      | |
|     const generateFreshTestData = () => { | |
|         log('\n🔄 Generating fresh test data...'); | |
|         try { | |
|             // Ensure .generated directory exists | |
|             if (!existsSync('.generated')) { | |
|                 mkdirSync('.generated', { recursive: true }); | |
|             } | |
| 
 | |
|             // Execute the generate_data.ts script synchronously | |
|             log('Running generate_data.ts...'); | |
|             execSync('npx ts-node test-scripts/generate_data.ts', {  | |
|                 stdio: 'inherit', | |
|                 encoding: 'utf8' | |
|             }); | |
| 
 | |
|             // Read and validate the generated files | |
|             const testEnvPath = '.generated/test-env.json'; | |
|             const contactsPath = '.generated/contacts.json'; | |
|              | |
|             if (!existsSync(testEnvPath) || !existsSync(contactsPath)) { | |
|                 throw new Error('Generated files not found after running generate_data.ts'); | |
|             } | |
| 
 | |
|             const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8')); | |
|             const contacts = JSON.parse(readFileSync(contactsPath, 'utf8')); | |
| 
 | |
|             // Validate required fields | |
|             if (!testEnv.CONTACT1_DID) { | |
|                 throw new Error('CONTACT1_DID missing from generated test data'); | |
|             } | |
| 
 | |
|             log('Generated test data:', { | |
|                 testEnv: testEnv, | |
|                 contacts: contacts | |
|             }); | |
| 
 | |
|             return { testEnv, contacts }; | |
|         } catch (error) { | |
|             log('❌ Test data generation failed:', error); | |
|             throw error; | |
|         } | |
|     }; | |
| 
 | |
|     try { | |
|         // Try to read existing data or generate fresh data | |
|         const testEnvPath = '.generated/test-env.json'; | |
|         const contactsPath = '.generated/contacts.json'; | |
|          | |
|         let testData; | |
|          | |
|         // If either file is missing or invalid, generate fresh data | |
|         if (!existsSync(testEnvPath) || !existsSync(contactsPath)) { | |
|             testData = generateFreshTestData(); | |
|         } else { | |
|             try { | |
|                 const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8')); | |
|                 const contacts = JSON.parse(readFileSync(contactsPath, 'utf8')); | |
|                  | |
|                 // Validate required fields | |
|                 if (!testEnv.CLAIM_ID || !testEnv.CONTACT1_DID) { | |
|                     log('⚠️ Existing test data missing required fields, regenerating...'); | |
|                     testData = generateFreshTestData(); | |
|                 } else { | |
|                     testData = { testEnv, contacts }; | |
|                 } | |
|             } catch (error) { | |
|                 log('⚠️ Error reading existing test data, regenerating...'); | |
|                 testData = generateFreshTestData(); | |
|             } | |
|         } | |
|          | |
|         // Final validation of data | |
|         if (!testData.testEnv.CLAIM_ID || !testData.testEnv.CONTACT1_DID) { | |
|             throw new Error('Test data validation failed even after generation'); | |
|         } | |
|          | |
|         log('✅ Test data validated successfully'); | |
|         log('📄 Test Environment:', JSON.stringify(testData.testEnv, null, 2)); | |
|          | |
|         return testData; | |
|          | |
|     } catch (error) { | |
|         log(`❌ Test data validation failed: ${error.message}`); | |
|         throw error; | |
|     } | |
| }; | |
| 
 | |
| /** | |
|  * Run deeplink tests  | |
|  * Optionally tests deeplinks if the test data is available | |
|  *  | |
|  * @param {function} log - Logging function | |
|  * @returns {Promise<void>} | |
|  */ | |
| const runDeeplinkTests = async (log) => { | |
|     log('\n=== Starting Deeplink Tests ==='); | |
|      | |
|     // Validate test data before proceeding | |
|     let testEnv, contacts; | |
|     try { | |
|         ({ testEnv, contacts } = validateTestData(log)); | |
|     } catch (error) { | |
|         log('❌ Cannot proceed with tests due to invalid test data'); | |
|         log(`Error: ${error.message}`); | |
|         log('Please ensure test data is properly generated before running tests'); | |
|         process.exit(1); // Exit with error code | |
|     } | |
|      | |
|     // Now we can safely create the deeplink tests knowing we have valid data | |
|         const deeplinkTests = [ | |
|             { | |
|             url: `timesafari://claim/${testEnv.CLAIM_ID}`, | |
|                 description: 'Claim view' | |
|             }, | |
|             { | |
|             url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`, | |
|                 description: 'Claim certificate view' | |
|             }, | |
|             { | |
|             url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.CLAIM_ID}`, | |
|                 description: 'Raw claim addition' | |
|             }, | |
|             { | |
|                 url: 'timesafari://did/test', | |
|                 description: 'DID view with test identifier' | |
|             }, | |
|             { | |
|                 url: `timesafari://did/${testEnv.CONTACT1_DID}`, | |
|                 description: 'DID view with contact DID' | |
|             }, | |
|             { | |
|             url: (() => { | |
|                 if (!testEnv?.CONTACT1_DID) { | |
|                     throw new Error('Cannot construct contact-edit URL: CONTACT1_DID is missing'); | |
|                 } | |
|                 const url = `timesafari://contact-edit/${testEnv.CONTACT1_DID}`; | |
|                 log('Created contact-edit URL:', url); | |
|                 return url; | |
|             })(), | |
|                 description: 'Contact editing' | |
|             }, | |
|             { | |
|                 url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`, | |
|                 description: 'Contacts import' | |
|             } | |
|         ]; | |
| 
 | |
|     // Log the final test configuration | |
|     log('\n5. Final Test Configuration:'); | |
|     deeplinkTests.forEach((test, i) => { | |
|         log(`\nTest ${i + 1}:`); | |
|         log(`Description: ${test.description}`); | |
|         log(`URL: ${test.url}`); | |
|     }); | |
|      | |
|     // Show instructions for iOS security dialogs | |
|     log('\n📱 IMPORTANT: iOS Security Dialog Instructions:'); | |
|     log('1. Each deeplink test will trigger a security confirmation dialog'); | |
|     log('2. You MUST click "Open" on each dialog to continue testing'); | |
|     log('3. The app must be running in the FOREGROUND'); | |
|     log('4. You will need to press Enter in this terminal after handling each dialog'); | |
|     log('5. You can abort the testing process by pressing Ctrl+C\n'); | |
|      | |
|     // Ensure app is in foreground | |
|     log('⚠️ IMPORTANT: Please make sure the app is in the FOREGROUND now'); | |
|     await question('Press Enter when the app is visible and in the foreground...'); | |
|      | |
|     try { | |
|         // Execute each test | |
|         let testsCompleted = 0; | |
|         let testsSkipped = 0; | |
|          | |
|         for (const test of deeplinkTests) { | |
|             // Show upcoming test info before execution | |
|             log('\n📱 NEXT TEST:'); | |
|             log('------------------------'); | |
|             log(`Description: ${test.description}`); | |
|             log(`URL to test: ${test.url}`); | |
|             log('------------------------'); | |
|              | |
|             // Clear prompt for user action | |
|             await question('\n⏎  Press Enter to execute this test (or Ctrl+C to quit)...'); | |
|              | |
|             try { | |
|                 log('🚀 Executing deeplink test...'); | |
|                 log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue'); | |
|                  | |
|                 execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' }); | |
|                 log(`✅ Successfully executed: ${test.description}`); | |
|                 testsCompleted++; | |
|                  | |
|                 // Show progress | |
|                 log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`); | |
|                  | |
|                 // If there are more tests, show the next one | |
|                 if (testsCompleted < deeplinkTests.length) { | |
|                     const nextTest = deeplinkTests[testsCompleted]; | |
|                     log('\n⏭️  NEXT UP:'); | |
|                     log('------------------------'); | |
|                     log(`Next test will be: ${nextTest.description}`); | |
|                     log(`URL: ${nextTest.url}`); | |
|                     log('------------------------'); | |
|                     await question('\n⏎  Press Enter when ready for the next test...'); | |
|                 } | |
|             } catch (deeplinkError) { | |
|                 const errorMessage = deeplinkError.message || ''; | |
|                  | |
|                 // Handle specific error for URL scheme not registered | |
|                 if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) { | |
|                     log(`⚠️ URL scheme not properly handled: ${test.description}`); | |
|                     testsSkipped++; | |
|                 } else { | |
|                     log(`⚠️ Failed to execute deeplink test: ${test.description}`); | |
|                     log(`⚠️ Error: ${errorMessage}`); | |
|                 } | |
|                 log('⚠️ Continuing with next test...'); | |
|                  | |
|                 // Show next test info after error handling | |
|                 if (testsCompleted + testsSkipped < deeplinkTests.length) { | |
|                     const nextTest = deeplinkTests[testsCompleted + testsSkipped]; | |
|                     log('\n⏭️  NEXT UP:'); | |
|                     log('------------------------'); | |
|                     log(`Next test will be: ${nextTest.description}`); | |
|                     log(`URL: ${nextTest.url}`); | |
|                     log('------------------------'); | |
|                     await question('\n⏎  Press Enter when ready for the next test...'); | |
|                 } | |
|             } | |
|         } | |
| 
 | |
|         log('\n🎉 All deeplink tests completed!'); | |
|         log(`✅ Successful: ${testsCompleted}`); | |
|         log(`⚠️ Skipped: ${testsSkipped}`); | |
|          | |
|         if (testsSkipped > 0) { | |
|             log('\n📝 Note about skipped tests:'); | |
|             log('1. The app needs to have the URL scheme registered in Info.plist'); | |
|             log('2. The app needs to be rebuilt after registering the URL scheme'); | |
|             log('3. The app must be running in the foreground for deeplink tests to work'); | |
|             log('4. iOS security dialogs must be manually approved for each deeplink test'); | |
|             log('5. If these conditions are met and tests still fail, check URL handling in the app code'); | |
|         } | |
|     } catch (error) { | |
|         log(`❌ Deeplink tests setup failed: ${error.message}`); | |
|         log('⚠️ Deeplink tests might be unavailable or test data is missing'); | |
|     } | |
| }; | |
| 
 | |
| // Check and register URL scheme if needed | |
| const checkAndRegisterUrlScheme = (log) => { | |
|     log('🔍 Checking if URL scheme is registered in Info.plist...'); | |
|      | |
|     const infoPlistPath = 'ios/App/App/Info.plist'; | |
|      | |
|     // Check if Info.plist exists | |
|     if (!existsSync(infoPlistPath)) { | |
|         log('⚠️ Info.plist not found at: ' + infoPlistPath); | |
|         return false; | |
|     } | |
|      | |
|     // Read Info.plist content | |
|     const infoPlistContent = readFileSync(infoPlistPath, 'utf8'); | |
|      | |
|     // Check if URL scheme is already registered | |
|     if (infoPlistContent.includes('<string>timesafari</string>')) { | |
|         log('✅ URL scheme "timesafari://" is already registered in Info.plist'); | |
|         return true; | |
|     } | |
|      | |
|     log('⚠️ URL scheme "timesafari://" is not registered in Info.plist'); | |
|     log('⚠️ Attempting to register the URL scheme automatically...'); | |
|      | |
|     try { | |
|         // Look for the closing dict tag to insert our URL types | |
|         const closingDictIndex = infoPlistContent.lastIndexOf('</dict>'); | |
|         if (closingDictIndex === -1) { | |
|             log('⚠️ Could not find closing dict tag in Info.plist'); | |
|             return false; | |
|         } | |
|          | |
|         // Create URL types entry | |
|         const urlTypesEntry = ` | |
| 	<key>CFBundleURLTypes</key> | |
| 	<array> | |
| 		<dict> | |
| 			<key>CFBundleURLName</key> | |
| 			<string>app.timesafari</string> | |
| 			<key>CFBundleURLSchemes</key> | |
| 			<array> | |
| 				<string>timesafari</string> | |
| 			</array> | |
| 		</dict> | |
| 	</array>`; | |
|          | |
|         // Insert URL types entry before closing dict | |
|         const updatedPlistContent =  | |
|             infoPlistContent.substring(0, closingDictIndex) +  | |
|             urlTypesEntry +  | |
|             infoPlistContent.substring(closingDictIndex); | |
|          | |
|         // Write updated content back to Info.plist | |
|         const { writeFileSync } = require('fs'); | |
|         writeFileSync(infoPlistPath, updatedPlistContent, 'utf8'); | |
|          | |
|         log('✅ URL scheme "timesafari://" registered in Info.plist'); | |
|         log('⚠️ You will need to rebuild the app for changes to take effect'); | |
|         return true; | |
|     } catch (error) { | |
|         log(`⚠️ Failed to register URL scheme: ${error.message}`); | |
|         return false; | |
|     } | |
| }; | |
| 
 | |
| // Helper function to get the app identifier from package.json or capacitor config | |
| const getAppIdentifier = () => { | |
|     try { | |
|         // Try to read from capacitor.config.ts/js/json | |
|         if (existsSync('capacitor.config.json')) { | |
|             const config = JSON.parse(readFileSync('capacitor.config.json', 'utf8')); | |
|             return config.appId; | |
|         }  | |
|         if (existsSync('capacitor.config.js')) { | |
|             // We can't directly require the file, but we can try to extract the appId | |
|             const content = readFileSync('capacitor.config.js', 'utf8'); | |
|             const match = content.match(/appId:\s*['"]([^'"]+)['"]/); | |
|             if (match && match[1]) return match[1]; | |
|         } | |
|         if (existsSync('capacitor.config.ts')) { | |
|             // Similar approach for TypeScript | |
|             const content = readFileSync('capacitor.config.ts', 'utf8'); | |
|             const match = content.match(/appId:\s*['"]([^'"]+)['"]/); | |
|             if (match && match[1]) return match[1]; | |
|         } | |
|          | |
|         // Fall back to package.json | |
|         const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); | |
|         if (packageJson.capacitor && packageJson.capacitor.appId) { | |
|             return packageJson.capacitor.appId; | |
|         } | |
|          | |
|         // Default fallback | |
|         return 'app.timesafari'; | |
|     } catch (error) { | |
|         console.error('Error getting app identifier:', error); | |
|         return 'app.timesafari'; // Default fallback | |
|     } | |
| }; | |
| 
 | |
| /** | |
|  * Runs the complete iOS test suite including build and testing | |
|  *  | |
|  * The function performs the following steps: | |
|  * 1. Cleans and resets the iOS platform | |
|  * 2. Verifies prerequisites and project setup | |
|  * 3. Syncs the Capacitor project with latest web build | |
|  * 4. Builds the app using xcodebuild | |
|  * 5. Optionally runs tests if configured | |
|  * 6. Launches the app in the simulator | |
|  *  | |
|  * If no simulator is running, it automatically selects and boots one. | |
|  *  | |
|  * @async | |
|  * @throws {Error} If any step in the build process fails | |
|  *  | |
|  * @example | |
|  * runIosTests().catch(error => { | |
|  *     console.error('Test execution failed:', error); | |
|  *     process.exit(1); | |
|  * }); | |
|  */ | |
| async function runIosTests() { | |
|     // Create build_logs directory if it doesn't exist | |
|     if (!existsSync('build_logs')) { | |
|         mkdirSync('build_logs'); | |
|     } | |
| 
 | |
|     const logFile = getLogFileName(); | |
|     const log = createLogger(logFile); | |
| 
 | |
|     try { | |
|         log('🚀 Starting iOS build and test process...'); | |
| 
 | |
|         // Clean and reset iOS platform first | |
|         const cleanSuccess = await cleanIosPlatform(log); | |
|         if (!cleanSuccess) { | |
|             throw new Error('Failed to clean and reset iOS platform. Please check the logs for details.'); | |
|         } | |
| 
 | |
|         // Check prerequisites | |
|         await checkPrerequisites(log); | |
| 
 | |
|         // Generate test data | |
|         await generateTestData(log); | |
| 
 | |
|         // Verify Xcode installation | |
|         verifyXcodeInstallation(log); | |
|          | |
|         // Check for simulator or boot one if needed | |
|         const simulator = await checkSimulator(log); | |
|          | |
|         // Configure iOS project | |
|         await configureIosProject(log); | |
|          | |
|         // Build and test using the selected simulator | |
|         await buildAndTestIos(log, simulator); | |
|          | |
|         // Run the app in the simulator | |
|         await runIosApp(log, simulator); | |
| 
 | |
|         // Run deeplink tests after app is installed | |
|         await runDeeplinkTests(log); | |
| 
 | |
|         log('🎉 iOS build and test process completed successfully'); | |
|         log(`📝 Full build log available at: ${logFile}`); | |
|     } catch (error) { | |
|         log(`❌ iOS tests failed: ${error.message}`); | |
|         log(`📝 Check build log for details: ${logFile}`); | |
|         process.exit(1); | |
|     } | |
| } | |
| 
 | |
| // Execute the test suite | |
| runIosTests(); 
 |