/** * @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. Sync Capacitor project with latest web build * 2. Build app for iOS simulator * 3. 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 } = require('fs'); // 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); }; }; // Check for iOS simulator const checkSimulator = async (log) => { log('๐Ÿ” Checking for iOS simulator...'); const simulators = execSync('xcrun simctl list devices available').toString(); const bootedDevices = simulators.split('\n') .filter(line => line.includes('Booted')) .map(line => line.match(/(.*?)\s+\((.*?)\)/)?.[1]) .filter(Boolean); if (bootedDevices.length === 0) { throw new Error('No iOS simulator running. Please start a simulator first.'); } log(`๐Ÿ“ฑ Found ${bootedDevices.length} simulator(s): ${bootedDevices.join(', ')}`); return bootedDevices; }; // 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('๐Ÿ”„ Generating test data...'); try { execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); log('โœ… Test data generated successfully'); } catch (error) { throw new Error(`Failed to generate test data: ${error.message}`); } }; // 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('๐Ÿ“ฑ Syncing Capacitor project...'); execSync('npx cap sync ios', { stdio: 'inherit' }); log('โœ… Capacitor sync completed'); log('โš™๏ธ Installing CocoaPods dependencies...'); execSync('cd ios/App && pod install', { stdio: 'inherit' }); log('โœ… CocoaPods installation completed'); }; // Build and test iOS project const buildAndTestIos = async (log) => { log('๐Ÿ—๏ธ Building iOS project...'); execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' }); log('โœ… Xcode clean completed'); execSync('cd ios/App && xcodebuild build-for-testing -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=iPhone 14"', { stdio: 'inherit' }); log('โœ… Xcode build completed'); log('๐Ÿงช Running iOS tests...'); execSync('cd ios/App && xcodebuild test-without-building -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=iPhone 14"', { stdio: 'inherit' }); log('โœ… iOS tests completed'); }; // Run the app const runIosApp = async (log) => { log('๐Ÿ“ฑ Running app in simulator...'); execSync('npx cap run ios', { stdio: 'inherit' }); log('โœ… App launched successfully'); }; // Run deeplink tests const runDeeplinkTests = async (log) => { log('๐Ÿ”— Starting deeplink tests...'); try { // Load test data const testEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8')); const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8')); const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8')); // Test URLs const deeplinkTests = [ { url: `timesafari://claim/${claimDetails.claim_id}`, description: 'Claim view' }, { url: `timesafari://claim-cert/${claimDetails.claim_id}`, description: 'Claim certificate view' }, { url: `timesafari://claim-add-raw/${claimDetails.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: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`, description: 'Contact editing' }, { url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`, description: 'Contacts import' } ]; // Execute each test for (const test of deeplinkTests) { log(`\n๐Ÿ”— Testing deeplink: ${test.description}`); log(`URL: ${test.url}`); execSync(`xcrun simctl openurl booted "${test.url}"`); log(`โœ… Successfully executed: ${test.description}`); // Wait between tests await new Promise(resolve => setTimeout(resolve, 5000)); } log('โœ… All deeplink tests completed successfully'); } catch (error) { log('โŒ Deeplink tests failed'); throw error; } }; /** * Runs the complete iOS test suite including build and testing * * The function performs the following steps: * 1. Syncs the Capacitor project with latest web build * 2. Builds and tests the app using xcodebuild * * Note: This function requires a running iOS simulator. The test will * fail if no simulator is available or if it's not in a booted state. * * @async * @throws {Error} If any step in the build or test 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...'); // Generate test data first await generateTestData(log); await checkSimulator(log); verifyXcodeInstallation(log); await buildWebAssets(log); await configureIosProject(log); await buildAndTestIos(log); await runIosApp(log); // 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();