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(); | |
| }); 
 |