You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							451 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							451 lines
						
					
					
						
							15 KiB
						
					
					
				
								/**
							 | 
						|
								 * @fileoverview Android test runner for Capacitor-based mobile app
							 | 
						|
								 * 
							 | 
						|
								 * This script handles the build, installation, and testing of the Android app.
							 | 
						|
								 * It ensures the app is properly synced, built, installed on a device/emulator,
							 | 
						|
								 * and runs the test suite.
							 | 
						|
								 * 
							 | 
						|
								 * Process flow:
							 | 
						|
								 * 1. Sync Capacitor project with latest web build
							 | 
						|
								 * 2. Build debug APK
							 | 
						|
								 * 3. Install APK on connected device/emulator
							 | 
						|
								 * 4. Run instrumented tests
							 | 
						|
								 * 
							 | 
						|
								 * Prerequisites:
							 | 
						|
								 * - Android SDK installed and ANDROID_HOME set
							 | 
						|
								 * - Gradle installed and in PATH
							 | 
						|
								 * - Connected Android device or running emulator
							 | 
						|
								 * - Capacitor Android platform added to project
							 | 
						|
								 * 
							 | 
						|
								 * Exit codes:
							 | 
						|
								 * - 0: Tests completed successfully
							 | 
						|
								 * - 1: Build, installation, or test failure
							 | 
						|
								 * 
							 | 
						|
								 * @example
							 | 
						|
								 * // Run directly
							 | 
						|
								 * node scripts/test-android.js
							 | 
						|
								 * 
							 | 
						|
								 * // Run via npm script
							 | 
						|
								 * npm run test:android
							 | 
						|
								 * 
							 | 
						|
								 * @requires child_process
							 | 
						|
								 * @requires path
							 | 
						|
								 * @requires readline
							 | 
						|
								 * 
							 | 
						|
								 * @author TimeSafari Team
							 | 
						|
								 * @license MIT
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								const { execSync } = require('child_process');
							 | 
						|
								const { join } = require('path');
							 | 
						|
								const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } = require('fs');
							 | 
						|
								const readline = require('readline');
							 | 
						|
								const rl = readline.createInterface({
							 | 
						|
								    input: process.stdin,
							 | 
						|
								    output: process.stdout
							 | 
						|
								});
							 | 
						|
								
							 | 
						|
								const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
							 | 
						|
								
							 | 
						|
								// 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/android-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 connected Android devices
							 | 
						|
								const checkConnectedDevices = async (log) => {
							 | 
						|
								    log('🔍 Checking for Android devices...');
							 | 
						|
								    const devices = execSync('adb devices').toString();
							 | 
						|
								    const connectedDevices = devices.split('\n')
							 | 
						|
								        .slice(1)
							 | 
						|
								        .filter(line => line.includes('device'))
							 | 
						|
								        .map(line => line.split('\t')[0])
							 | 
						|
								        .filter(Boolean);
							 | 
						|
								
							 | 
						|
								    if (connectedDevices.length === 0) {
							 | 
						|
								        throw new Error('No Android devices or emulators connected. Please connect a device or start an emulator.');
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    log(`📱 Found ${connectedDevices.length} device(s): ${connectedDevices.join(', ')}`);
							 | 
						|
								    return connectedDevices;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Verify Java installation
							 | 
						|
								const verifyJavaInstallation = (log) => {
							 | 
						|
								    log('🔍 Checking Java...');
							 | 
						|
								    const javaHome = process.env.JAVA_HOME;
							 | 
						|
								    if (!existsSync(javaHome)) {
							 | 
						|
								        throw new Error(`Required Java not found at ${javaHome}. Please install OpenJDK.`);
							 | 
						|
								    }
							 | 
						|
								    log('✅ Java found');
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Generate test data using generate_data.ts
							 | 
						|
								const generateTestData = async (log) => {
							 | 
						|
								    log('🔄 Generating test data...');
							 | 
						|
								    
							 | 
						|
								    // Create .generated directory if it doesn't exist
							 | 
						|
								    if (!existsSync('.generated')) {
							 | 
						|
								        mkdirSync('.generated', { recursive: true });
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								        // Generate test data
							 | 
						|
								        const testData = {
							 | 
						|
								            CONTACT1_DID: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
							 | 
						|
								            CLAIM_ID: "01JPVVX7FH0EKQWTQY9HTXZQDZ"
							 | 
						|
								        };
							 | 
						|
								
							 | 
						|
								        const claimDetails = {
							 | 
						|
								            claim_id: "01JPVVX7FH0EKQWTQY9HTXZQDZ",
							 | 
						|
								            issuedAt: "2025-03-21T08:07:57ZZ",
							 | 
						|
								            issuer: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"
							 | 
						|
								        };
							 | 
						|
								
							 | 
						|
								        const contacts = [
							 | 
						|
								            {
							 | 
						|
								                did: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
							 | 
						|
								                name: "Test Contact"
							 | 
						|
								            }
							 | 
						|
								        ];
							 | 
						|
								
							 | 
						|
								        // Write files
							 | 
						|
								        log('📝 Writing test data files...');
							 | 
						|
								        writeFileSync('.generated/test-env.json', JSON.stringify(testData, null, 2));
							 | 
						|
								        writeFileSync('.generated/claim_details.json', JSON.stringify(claimDetails, null, 2));
							 | 
						|
								        writeFileSync('.generated/contacts.json', JSON.stringify(contacts, null, 2));
							 | 
						|
								
							 | 
						|
								        // Verify files were written
							 | 
						|
								        log('✅ Verifying test data files...');
							 | 
						|
								        const files = [
							 | 
						|
								            '.generated/test-env.json',
							 | 
						|
								            '.generated/claim_details.json',
							 | 
						|
								            '.generated/contacts.json'
							 | 
						|
								        ];
							 | 
						|
								
							 | 
						|
								        for (const file of files) {
							 | 
						|
								            if (!existsSync(file)) {
							 | 
						|
								                throw new Error(`Failed to create ${file}`);
							 | 
						|
								            }
							 | 
						|
								            log(`✅ Created ${file}`);
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        log('✅ Test data generated successfully');
							 | 
						|
								    } catch (error) {
							 | 
						|
								        log(`❌ Failed to generate test data: ${error.message}`);
							 | 
						|
								        throw error;
							 | 
						|
								    }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Parse shell environment file
							 | 
						|
								const parseEnvFile = (filePath) => {
							 | 
						|
								    const content = readFileSync(filePath, 'utf8');
							 | 
						|
								    const env = {};
							 | 
						|
								    content.split('\n').forEach(line => {
							 | 
						|
								        const match = line.match(/^export\s+(\w+)="(.+)"$/);
							 | 
						|
								        if (match) {
							 | 
						|
								            env[match[1]] = match[2];
							 | 
						|
								        }
							 | 
						|
								    });
							 | 
						|
								    return env;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Run individual deeplink test
							 | 
						|
								const executeDeeplink = async (url, description, log) => {
							 | 
						|
								    log(`\n🔗 Testing deeplink: ${description}`);
							 | 
						|
								    log(`URL: ${url}`);
							 | 
						|
								    
							 | 
						|
								    try {
							 | 
						|
								        // Stop the app before executing the deep link
							 | 
						|
								        execSync('adb shell am force-stop app.timesafari.app');
							 | 
						|
								        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
							 | 
						|
								        
							 | 
						|
								        execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
							 | 
						|
								        log(`✅ Successfully executed: ${description}`);
							 | 
						|
								        
							 | 
						|
								        // Wait for app to load content
							 | 
						|
								        await new Promise(resolve => setTimeout(resolve, 3000)); 
							 | 
						|
								        
							 | 
						|
								        // Wait for user confirmation before continuing
							 | 
						|
								        await question('\n⏎  Press Enter to continue to next test (or Ctrl+C to quit)...');
							 | 
						|
								        
							 | 
						|
								        // Press Back button to ensure app is in consistent state
							 | 
						|
								        log(`📱 Sending keystroke (BACK) to device...`);
							 | 
						|
								        execSync('adb shell input keyevent KEYCODE_BACK');
							 | 
						|
								        
							 | 
						|
								        // Small delay after keystroke
							 | 
						|
								        await new Promise(resolve => setTimeout(resolve, 2000));
							 | 
						|
								    } catch (error) {
							 | 
						|
								        log(`❌ Failed to execute deeplink: ${description}`);
							 | 
						|
								        log(`Error: ${error.message}`);
							 | 
						|
								        throw error;
							 | 
						|
								    }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Run all 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'
							 | 
						|
								            }
							 | 
						|
								        ];
							 | 
						|
								
							 | 
						|
								        // Show test plan
							 | 
						|
								        log('\n📋 Test Plan:');
							 | 
						|
								        deeplinkTests.forEach((test, i) => {
							 | 
						|
								            log(`${i + 1}. ${test.description}`);
							 | 
						|
								        });
							 | 
						|
								
							 | 
						|
								        // Execute each test
							 | 
						|
								        let testsCompleted = 0;
							 | 
						|
								        for (const test of deeplinkTests) {
							 | 
						|
								            // Show progress
							 | 
						|
								            log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
							 | 
						|
								            
							 | 
						|
								            // Show upcoming test info
							 | 
						|
								            log('\n📱 NEXT TEST:');
							 | 
						|
								            log('------------------------');
							 | 
						|
								            log(`Description: ${test.description}`);
							 | 
						|
								            log(`URL: ${test.url}`);
							 | 
						|
								            log('------------------------');
							 | 
						|
								
							 | 
						|
								            await executeDeeplink(test.url, test.description, log);
							 | 
						|
								            testsCompleted++;
							 | 
						|
								
							 | 
						|
								            // 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('------------------------');
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        log('\n🎉 All deeplink tests completed successfully!');
							 | 
						|
								        rl.close(); // Close readline interface when done
							 | 
						|
								    } catch (error) {
							 | 
						|
								        log('❌ Deeplink tests failed');
							 | 
						|
								        rl.close(); // Close readline interface on error
							 | 
						|
								        throw error;
							 | 
						|
								    }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// 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 Android project
							 | 
						|
								const configureAndroidProject = async (log) => {
							 | 
						|
								    log('📱 Syncing Capacitor project...');
							 | 
						|
								    execSync('npx cap sync android', { stdio: 'inherit' });
							 | 
						|
								    log('✅ Capacitor sync completed');
							 | 
						|
								
							 | 
						|
								    log('⚙️ Configuring Gradle properties...');
							 | 
						|
								    const gradleProps = 'android/gradle.properties';
							 | 
						|
								    
							 | 
						|
								    // Create file if it doesn't exist
							 | 
						|
								    if (!existsSync(gradleProps)) {
							 | 
						|
								        execSync('touch android/gradle.properties');
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Check if line exists without using grep
							 | 
						|
								    const gradleContent = readFileSync(gradleProps, 'utf8');
							 | 
						|
								    if (!gradleContent.includes('android.suppressUnsupportedCompileSdk=34')) {
							 | 
						|
								        execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties');
							 | 
						|
								        log('✅ Added SDK suppression to gradle.properties');
							 | 
						|
								    } else {
							 | 
						|
								        log('✅ SDK suppression already configured in gradle.properties');
							 | 
						|
								    }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Build and test Android project
							 | 
						|
								const buildAndTestAndroid = async (log, env) => {
							 | 
						|
								    log('🏗️ Building Android project...');
							 | 
						|
								    
							 | 
						|
								    // Kill and restart ADB server first
							 | 
						|
								    try {
							 | 
						|
								        log('🔄 Restarting ADB server...');
							 | 
						|
								        execSync('adb kill-server', { stdio: 'inherit' });
							 | 
						|
								        await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
							 | 
						|
								        execSync('adb start-server', { stdio: 'inherit' });
							 | 
						|
								        await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s
							 | 
						|
								        
							 | 
						|
								        // Verify device connection
							 | 
						|
								        const devices = execSync('adb devices').toString();
							 | 
						|
								        if (!devices.includes('\tdevice')) {
							 | 
						|
								            throw new Error('No devices connected after ADB restart');
							 | 
						|
								        }
							 | 
						|
								        log('✅ ADB server restarted successfully');
							 | 
						|
								    } catch (error) {
							 | 
						|
								        log(`⚠️ ADB restart failed: ${error.message}`);
							 | 
						|
								        log('Continuing with build process...');
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Clean build
							 | 
						|
								    log('🧹 Cleaning project...');
							 | 
						|
								    execSync('cd android && ./gradlew clean', { stdio: 'inherit', env });
							 | 
						|
								    log('✅ Gradle clean completed');
							 | 
						|
								    
							 | 
						|
								    // Build
							 | 
						|
								    log('🏗️ Building project...');
							 | 
						|
								    execSync('cd android && ./gradlew build', { stdio: 'inherit', env });
							 | 
						|
								    log('✅ Gradle build completed');
							 | 
						|
								
							 | 
						|
								    // Run tests with retry
							 | 
						|
								    log('🧪 Running Android tests...');
							 | 
						|
								    let retryCount = 0;
							 | 
						|
								    const maxRetries = 3;
							 | 
						|
								
							 | 
						|
								    while (retryCount < maxRetries) {
							 | 
						|
								        try {
							 | 
						|
								            // Verify ADB connection before tests
							 | 
						|
								            execSync('adb devices', { stdio: 'inherit' });
							 | 
						|
								            
							 | 
						|
								            // Run the tests
							 | 
						|
								            execSync('cd android && ./gradlew connectedAndroidTest', { 
							 | 
						|
								                stdio: 'inherit', 
							 | 
						|
								                env,
							 | 
						|
								                timeout: 60000 // 1 minute timeout
							 | 
						|
								            });
							 | 
						|
								            log('✅ Android tests completed');
							 | 
						|
								            return;
							 | 
						|
								        } catch (error) {
							 | 
						|
								            retryCount++;
							 | 
						|
								            log(`⚠️ Test attempt ${retryCount} failed: ${error.message}`);
							 | 
						|
								            
							 | 
						|
								            if (retryCount < maxRetries) {
							 | 
						|
								                log('🔄 Restarting ADB and retrying...');
							 | 
						|
								                execSync('adb kill-server', { stdio: 'inherit' });
							 | 
						|
								                await new Promise(resolve => setTimeout(resolve, 2000));
							 | 
						|
								                execSync('adb start-server', { stdio: 'inherit' });
							 | 
						|
								                await new Promise(resolve => setTimeout(resolve, 3000));
							 | 
						|
								            } else {
							 | 
						|
								                throw new Error(`Android tests failed after ${maxRetries} attempts`);
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Run the app
							 | 
						|
								const runAndroidApp = async (log, env) => {
							 | 
						|
								    log('📱 Running app on device...');
							 | 
						|
								    execSync('npx cap run android', { stdio: 'inherit', env });
							 | 
						|
								    log('✅ App launched successfully');
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Runs the complete Android test suite including build, installation, and testing
							 | 
						|
								 * 
							 | 
						|
								 * The function performs the following steps:
							 | 
						|
								 * 1. Checks for connected devices/emulators
							 | 
						|
								 * 2. Ensures correct Java version is used
							 | 
						|
								 * 3. Checks if app is already installed
							 | 
						|
								 * 4. Syncs the Capacitor project with latest build
							 | 
						|
								 * 5. Builds and runs instrumented Android tests
							 | 
						|
								 * 
							 | 
						|
								 * @async
							 | 
						|
								 * @throws {Error} If any step in the build or test process fails
							 | 
						|
								 * 
							 | 
						|
								 * @example
							 | 
						|
								 * runAndroidTests().catch(error => {
							 | 
						|
								 *     console.error('Test execution failed:', error);
							 | 
						|
								 *     process.exit(1);
							 | 
						|
								 * });
							 | 
						|
								 */
							 | 
						|
								async function runAndroidTests() {
							 | 
						|
								    // 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 Android build and test process...');
							 | 
						|
								
							 | 
						|
								        // Generate test data first
							 | 
						|
								        await generateTestData(log);
							 | 
						|
								
							 | 
						|
								        await checkConnectedDevices(log);
							 | 
						|
								        await verifyJavaInstallation(log);
							 | 
						|
								        await buildWebAssets(log);
							 | 
						|
								        await configureAndroidProject(log);
							 | 
						|
								        const env = process.env;
							 | 
						|
								        await buildAndTestAndroid(log, env);
							 | 
						|
								        await runAndroidApp(log, env);
							 | 
						|
								
							 | 
						|
								        // Run deeplink tests after app is installed
							 | 
						|
								        await runDeeplinkTests(log);
							 | 
						|
								
							 | 
						|
								        log('🎉 Android build and test process completed successfully');
							 | 
						|
								        log(`📝 Full build log available at: ${logFile}`);
							 | 
						|
								    } catch (error) {
							 | 
						|
								        log(`❌ Android tests failed: ${error.message}`);
							 | 
						|
								        log(`📝 Check build log for details: ${logFile}`);
							 | 
						|
								        process.exit(1);
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Execute the test suite
							 | 
						|
								runAndroidTests();
							 | 
						|
								
							 | 
						|
								// Add cleanup handler for SIGINT
							 | 
						|
								process.on('SIGINT', () => {
							 | 
						|
								    rl.close();
							 | 
						|
								    process.exit();
							 | 
						|
								}); 
							 |