Files
crowd-funder-from-jason/scripts/test-ios.js
Matthew Raymer 474999dc9c feat(test): Comprehensive iOS test script overhaul with context-aware deeplink testing
* Add complete iOS platform cleanup and reset on each test run
* Implement interactive deeplink testing with keyboard controls between tests
* Add context awareness to verify app is running and in foreground
* Improve error handling and diagnostic messaging throughout
* Auto-register URL schemes and verify app state for reliable testing
Include prerequisites check for Xcode, CocoaPods and simulator availability
* Include prerequisites check for Xcode, CocoaPods and simulator availability
2025-03-18 00:34:17 -07:00

951 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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 } = 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);
};
};
/**
* 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('🔄 Generating test data...');
// Check if test-scripts directory exists
if (!existsSync('test-scripts')) {
log('⚠️ test-scripts directory not found');
log('⚠️ Current directory: ' + process.cwd());
// List directories to help debug
const { readdirSync } = require('fs');
log('📂 Directories in current path:');
try {
const files = readdirSync('.');
files.forEach(file => {
const isDir = existsSync(file) && require('fs').statSync(file).isDirectory();
log(`${isDir ? '📁' : '📄'} ${file}`);
});
} catch (err) {
log(`⚠️ Error listing directory: ${err.message}`);
}
} else {
log('✅ Found test-scripts directory');
// Check if generate_data.ts exists
if (existsSync('test-scripts/generate_data.ts')) {
log('✅ Found generate_data.ts');
} else {
log('⚠️ generate_data.ts not found in test-scripts directory');
// List files in test-scripts to help debug
const { readdirSync } = require('fs');
log('📂 Files in test-scripts:');
try {
const files = readdirSync('test-scripts');
files.forEach(file => {
log(`📄 ${file}`);
});
} catch (err) {
log(`⚠️ Error listing test-scripts: ${err.message}`);
}
}
}
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true });
}
try {
// Try to generate test data using the script
log('🔄 Running test data generation script...');
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
log('✅ Test data generation script completed');
// Verify the generated files exist
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying generated files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} was not generated`);
throw new Error(`Required file ${file} was not generated`);
} else {
log(`${file} exists`);
}
}
} catch (error) {
log(`⚠️ Failed to generate test data: ${error.message}`);
log('⚠️ Creating fallback test data...');
// Create minimal fallback test data
const fallbackTestEnv = {
"CONTACT1_DID": "did:example:123456789",
"APP_URL": "https://app.timesafari.example"
};
const fallbackClaimDetails = {
"claim_id": "claim_12345",
"title": "Test Claim",
"description": "This is a test claim"
};
const fallbackContacts = [
{
"id": "contact1",
"name": "Test Contact",
"did": "did:example:123456789"
}
];
// Use writeFileSync to overwrite any existing files
const { writeFileSync } = require('fs');
writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(fallbackClaimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
log('✅ Fallback test data created');
// Verify files were created
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying fallback files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Failed to create ${file}`);
} else {
log(`✅ Created ${file}`);
}
}
}
};
// 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');
};
/**
* 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('🔗 Starting deeplink tests...');
// Import readline module for user input
const readline = require('readline');
// Create readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Promisify the question method
const question = (query) => new Promise(resolve => rl.question(query, resolve));
// Register URL scheme if needed
checkAndRegisterUrlScheme(log);
// Check if test data files exist first
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} does not exist`);
log('⚠️ Skipping deeplink tests');
rl.close();
return;
}
}
// Check if our app is actually running in the simulator
log('🔍 Checking if app is currently running in simulator...');
try {
const runningApps = execSync('xcrun simctl listapps booted').toString();
const appIdentifier = getAppIdentifier();
if (!runningApps.includes(appIdentifier)) {
log('⚠️ The app does not appear to be running in the simulator.');
const shouldLaunch = await question('Would you like to launch the app now? (y/n): ');
if (shouldLaunch.toLowerCase() === 'y' || shouldLaunch.toLowerCase() === 'yes') {
// Try launching the app again
log('🚀 Launching app in simulator...');
const simulatorInfo = JSON.parse(execSync('xcrun simctl list -j devices booted').toString());
const booted = Object.values(simulatorInfo.devices)
.flat()
.find(device => device.state === 'Booted');
if (booted) {
execSync(`npx cap run ios --target="${booted.udid}"`, { stdio: 'inherit' });
log('✅ App launched');
} else {
log('⚠️ No booted simulator found');
}
} else {
log('⚠️ Deeplink tests require the app to be running');
log('⚠️ Please launch the app manually and restart the tests');
rl.close();
return;
}
} else {
log('✅ App is running in simulator');
}
} catch (error) {
log(`⚠️ Unable to check if app is running: ${error.message}`);
log('⚠️ Proceeding with deeplink tests, but they may fail if app is not running');
}
// 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 {
// Load test data
log('📂 Loading test data from .generated directory');
let testEnv, claimDetails, contacts;
try {
const testEnvContent = readFileSync('.generated/test-env.json', 'utf8');
testEnv = JSON.parse(testEnvContent);
log('✅ Loaded test-env.json');
} catch (error) {
log(`⚠️ Failed to load test-env.json: ${error.message}`);
rl.close();
return;
}
try {
const claimDetailsContent = readFileSync('.generated/claim_details.json', 'utf8');
claimDetails = JSON.parse(claimDetailsContent);
log('✅ Loaded claim_details.json');
} catch (error) {
log(`⚠️ Failed to load claim_details.json: ${error.message}`);
rl.close();
return;
}
try {
const contactsContent = readFileSync('.generated/contacts.json', 'utf8');
contacts = JSON.parse(contactsContent);
log('✅ Loaded contacts.json');
} catch (error) {
log(`⚠️ Failed to load contacts.json: ${error.message}`);
rl.close();
return;
}
// Check if the app URL scheme is registered in the simulator
log('🔍 Checking if URL scheme is registered in simulator...');
try {
// Attempt to open a simple URL with the scheme
log('⚠️ A security dialog will appear - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "timesafari://test"`, { stdio: 'pipe' });
log('✅ URL scheme is registered and working');
} catch (error) {
const errorMessage = error.message || '';
// Check for the specific error code that indicates an unregistered URL scheme
if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) {
log('⚠️ URL scheme "timesafari://" is not registered in the app or app is not running');
log('⚠️ The scheme was added to Info.plist but the app may need to be rebuilt');
log('⚠️ Trying to continue with tests, but they may fail');
}
}
// Wait for user confirmation before proceeding
await question('Press Enter to continue with the tests...');
// 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
let testsCompleted = 0;
let testsSkipped = 0;
for (const test of deeplinkTests) {
try {
log(`\n🔗 Testing deeplink: ${test.description}`);
log(`URL: ${test.url}`);
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++;
// Wait for user to press Enter before continuing to next test
if (testsCompleted < deeplinkTests.length) {
await question('Press Enter to continue to 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...');
// Wait for user to press Enter before continuing to next test
if (testsCompleted + testsSkipped < deeplinkTests.length) {
await question('Press Enter to continue to the next test...');
}
}
}
log(`✅ Deeplink tests completed: ${testsCompleted} successful, ${testsSkipped} skipped`);
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');
}
// Close readline interface
rl.close();
} catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Close readline interface
rl.close();
// Don't rethrow the error to prevent halting the process
}
};
// 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.app</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.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();