/** * @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 * * @author TimeSafari Team * @license MIT */ const { execSync } = require('child_process'); const { join } = require('path'); const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs'); // Format date as YYYY-MM-DD-HHMMSS const getLogFileName = () => { const now = new Date(); const date = now.toISOString().split('T')[0]; const time = now.toTimeString().split(' ')[0].replace(/:/g, ''); return `build_logs/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...'); try { execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); log('โœ… Test data generated successfully'); } catch (error) { throw new Error(`Failed to generate test data: ${error.message}`); } }; // 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 between deeplink tests await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s } 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 = parseEnvFile('.generated/test-env.sh'); const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8')); const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8')); // Test each deeplink const deeplinkTests = [ { url: `timesafari://claim/${claimDetails.claim_id}`, description: 'Claim view' }, { url: `timesafari://claim-cert/${claimDetails.claim_id}`, description: 'Claim certificate view' }, { url: `timesafari://claim-add-raw/${claimDetails.claim_id}`, description: 'Raw claim addition' }, { url: 'timesafari://did/test', description: 'DID view with test identifier' }, { url: `timesafari://did/${testEnv.CONTACT1_DID}`, description: 'DID view with contact DID' }, { url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`, description: 'Contact editing' }, { url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`, description: 'Contacts import' } ]; // Execute each test for (const test of deeplinkTests) { await executeDeeplink(test.url, test.description, log); } let succeeded = true; try { await executeDeeplink('timesafari://contactJunk', 'Non-existent deeplink', log); } catch (error) { log('โœ… Non-existent deeplink failed as expected'); succeeded = false; } finally { if (succeeded) { throw new Error('Non-existent deeplink should have failed'); } } log('โœ… All deeplink tests completed successfully'); } catch (error) { log('โŒ Deeplink tests failed'); 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'; if (!existsSync(gradleProps) || !execSync(`grep -q "android.suppressUnsupportedCompileSdk=34" ${gradleProps}`)) { execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties'); log('โœ… Added SDK suppression to gradle.properties'); } }; // Build and test Android project const buildAndTestAndroid = async (log, env) => { log('๐Ÿ—๏ธ Building Android project...'); execSync('cd android && ./gradlew clean', { stdio: 'inherit', env }); log('โœ… Gradle clean completed'); execSync('cd android && ./gradlew build', { stdio: 'inherit', env }); log('โœ… Gradle build completed'); log('๐Ÿงช Running Android tests...'); execSync('cd android && ./gradlew connectedAndroidTest', { stdio: 'inherit', env }); log('โœ… Android tests completed'); }; // 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();