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