/** * @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.capacitor?.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๐Ÿ” DEBUG: 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...'); 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,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} */ 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('timesafari')) { 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(''); if (closingDictIndex === -1) { log('โš ๏ธ Could not find closing dict tag in Info.plist'); return false; } // Create URL types entry const urlTypesEntry = ` CFBundleURLTypes CFBundleURLName app.timesafari.app CFBundleURLSchemes timesafari `; // 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.app'; } catch (error) { console.error('Error getting app identifier:', error); return 'app.timesafari.app'; // 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();