forked from trent_larson/crowd-funder-for-time-pwa
- Eliminated all debugging console statements from 7 files (194 deletions) - Fixed parsing errors from broken function calls in databaseUtil.ts - Resolved orphaned console.log parameters in util.ts and ImportAccountView.vue - Maintained legitimate logging in PlatformServiceFactory and registerServiceWorker - Reduced lint issues from 33 problems (11 errors + 22 warnings) to 16 warnings - All builds and core functionality verified working
939 lines
35 KiB
JavaScript
939 lines
35 KiB
JavaScript
/**
|
||
* @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();
|