forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add comprehensive route validation with zod schema - Create type-safe DeepLinkRoute enum for all valid routes - Add structured error handling for invalid routes - Redirect to error page with detailed feedback - Add better timeout handling in deeplink tests The changes improve robustness by: 1. Validating route paths before navigation 2. Providing detailed error messages for invalid links 3. Redirecting users to dedicated error pages 4. Adding parameter validation with specific feedback 5. Improving type safety across deeplink handling
306 lines
10 KiB
JavaScript
306 lines
10 KiB
JavaScript
/**
|
|
* @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 for app to load content
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
// Press a key (Back button) to ensure app is in consistent state
|
|
log(`📱 Sending keystroke (BACK) to device...`);
|
|
execSync('adb shell input keyevent KEYCODE_BACK');
|
|
|
|
// Wait a bit longer after keystroke before next test
|
|
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 = 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();
|