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

/**
* @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');
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();
});