Merge branch 'master' into electron_fix_20250317

This commit is contained in:
Matthew Raymer
2025-03-24 12:19:33 +00:00
59 changed files with 4833 additions and 1550 deletions

View File

@@ -30,6 +30,7 @@
*
* @requires child_process
* @requires path
* @requires readline
*
* @author TimeSafari Team
* @license MIT
@@ -37,7 +38,14 @@
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs');
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 = () => {
@@ -88,11 +96,57 @@ const verifyJavaInstallation = (log) => {
// 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 {
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
// 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) {
throw new Error(`Failed to generate test data: ${error.message}`);
log(`Failed to generate test data: ${error.message}`);
throw error;
}
};
@@ -116,14 +170,24 @@ const executeDeeplink = async (url, description, log) => {
try {
// Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari.app');
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 between deeplink tests
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s
// 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}`);
@@ -137,11 +201,11 @@ const runDeeplinkTests = async (log) => {
try {
// Load test data
const testEnv = parseEnvFile('.generated/test-env.sh');
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 each deeplink
// Test URLs
const deeplinkTests = [
{
url: `timesafari://claim/${claimDetails.claim_id}`,
@@ -173,26 +237,44 @@ const runDeeplinkTests = async (log) => {
}
];
// Execute each test
for (const test of deeplinkTests) {
await executeDeeplink(test.url, test.description, log);
}
// Show test plan
log('\n📋 Test Plan:');
deeplinkTests.forEach((test, i) => {
log(`${i + 1}. ${test.description}`);
});
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');
// 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(' All deeplink tests completed successfully');
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;
}
};
@@ -214,24 +296,88 @@ const configureAndroidProject = async (log) => {
log('⚙️ Configuring Gradle properties...');
const gradleProps = 'android/gradle.properties';
if (!existsSync(gradleProps) || !execSync(`grep -q "android.suppressUnsupportedCompileSdk=34" ${gradleProps}`)) {
// 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...');
execSync('cd android && ./gradlew connectedAndroidTest', { stdio: 'inherit', env });
log('✅ Android tests completed');
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
@@ -296,4 +442,10 @@ async function runAndroidTests() {
}
// Execute the test suite
runAndroidTests();
runAndroidTests();
// Add cleanup handler for SIGINT
process.on('SIGINT', () => {
rl.close();
process.exit();
});

View File

@@ -6,9 +6,11 @@
* web build and runs the test suite on a specified iOS simulator.
*
* Process flow:
* 1. Sync Capacitor project with latest web build
* 2. Build app for iOS simulator
* 3. Run XCTest suite
* 1. Clean and reset iOS platform (if needed)
* 2. Check prerequisites (Xcode, CocoaPods, Capacitor setup)
* 3. Sync Capacitor project with latest web build
* 4. Build app for iOS simulator
* 5. Run XCTest suite
*
* Prerequisites:
* - macOS operating system
@@ -38,7 +40,21 @@
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync, statSync, accessSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const { constants } = require('fs');
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Make sure to close readline at the end
process.on('SIGINT', () => {
rl.close();
process.exit();
});
// Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => {
@@ -58,6 +74,169 @@ const createLogger = (logFile) => {
};
};
/**
* Clean up and reset iOS platform
* This function completely removes and recreates the iOS platform to ensure a fresh setup
* @param {function} log - Logging function
* @returns {boolean} - Success status
*/
const cleanIosPlatform = async (log) => {
log('🧹 Cleaning iOS platform (complete reset)...');
// Check for package.json and capacitor.config.ts/js
if (!existsSync('package.json')) {
log('⚠️ package.json not found. Are you in the correct directory?');
throw new Error('package.json not found. Cannot continue without project configuration.');
}
log('✅ package.json exists');
const capacitorConfigExists =
existsSync('capacitor.config.ts') ||
existsSync('capacitor.config.js') ||
existsSync('capacitor.config.json');
if (!capacitorConfigExists) {
log('⚠️ Capacitor config file not found');
log('Creating minimal capacitor.config.ts...');
try {
// Get app name from package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
const appName = packageJson.name || 'App';
const appId = packageJson.capacitor?.appId || 'io.ionic.starter';
// Create a minimal capacitor config
const capacitorConfig = `
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: '${appId}',
appName: '${appName}',
webDir: 'dist',
bundledWebRuntime: false
};
export default config;
`.trim();
writeFileSync('capacitor.config.ts', capacitorConfig);
log('✅ Created capacitor.config.ts');
} catch (configError) {
log('⚠️ Failed to create Capacitor config file');
log('Please create a capacitor.config.ts file manually');
throw new Error('Capacitor configuration missing. Please configure manually.');
}
} else {
log('✅ Capacitor config exists');
}
// Check if the platform exists first
if (existsSync('ios')) {
log('🗑️ Removing existing iOS platform directory...');
try {
execSync('rm -rf ios', { stdio: 'inherit' });
log('✅ Existing iOS platform removed');
} catch (error) {
log(`⚠️ Error removing iOS platform: ${error.message}`);
log('⚠️ You may need to manually remove the ios directory');
return false;
}
}
// Rebuild web assets first to ensure they're available
log('🔄 Building web assets before adding iOS platform...');
try {
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');
} catch (error) {
log(`⚠️ Error building web assets: ${error.message}`);
log('⚠️ Continuing with platform addition, but it may fail if web assets are required');
}
// Add the platform back
log(' Adding iOS platform...');
try {
execSync('npx cap add ios', { stdio: 'inherit' });
log('✅ iOS platform added successfully');
// Verify critical files were created
if (!existsSync('ios/App/Podfile')) {
log('⚠️ Podfile was not created - something is wrong with the Capacitor setup');
return false;
}
if (!existsSync('ios/App/App/Info.plist')) {
log('⚠️ Info.plist was not created - something is wrong with the Capacitor setup');
return false;
}
log('✅ iOS platform setup verified - critical files exist');
return true;
} catch (error) {
log(`⚠️ Error adding iOS platform: ${error.message}`);
return false;
}
};
/**
* Check all prerequisites for iOS testing
* Verifies and attempts to install/initialize all required components
*/
const checkPrerequisites = async (log) => {
log('🔍 Checking prerequisites for iOS testing...');
// Check for macOS
if (process.platform !== 'darwin') {
throw new Error('iOS testing is only supported on macOS');
}
log('✅ Running on macOS');
// Verify Xcode installation
try {
const xcodeOutput = execSync('xcode-select -p').toString().trim();
log(`✅ Xcode command line tools found at: ${xcodeOutput}`);
} catch (error) {
log('⚠️ Xcode command line tools not found');
log('Please install Xcode from the App Store and run:');
log('xcode-select --install');
throw new Error('Xcode command line tools not found. Please install Xcode first.');
}
// Check Xcode version
try {
const xcodeVersionOutput = execSync('xcodebuild -version').toString().trim();
log(`✅ Xcode version: ${xcodeVersionOutput.split('\n')[0]}`);
} catch (error) {
log('⚠️ Unable to determine Xcode version');
}
// Check for CocoaPods
try {
const podVersionOutput = execSync('pod --version').toString().trim();
log(`✅ CocoaPods version: ${podVersionOutput}`);
} catch (error) {
log('⚠️ CocoaPods not found');
log('Attempting to install CocoaPods...');
try {
log('🔄 Installing CocoaPods via gem...');
execSync('gem install cocoapods', { stdio: 'inherit' });
log('✅ CocoaPods installed successfully');
} catch (gemError) {
log('⚠️ Failed to install CocoaPods via gem');
log('Please install CocoaPods manually:');
log('1. sudo gem install cocoapods');
log('2. brew install cocoapods');
throw new Error('CocoaPods installation failed. Please install manually.');
}
}
log('✅ All prerequisites for iOS testing are met');
return true;
};
// Check for iOS simulator
const checkSimulator = async (log) => {
log('🔍 Checking for iOS simulator...');
@@ -151,122 +330,84 @@ const verifyXcodeInstallation = (log) => {
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('🔄 Generating test data...');
log('\n🔍 DEBUG: Starting test data generation...');
// Check if test-scripts directory exists
if (!existsSync('test-scripts')) {
log('⚠️ test-scripts directory not found');
log('⚠️ Current directory: ' + process.cwd());
// List directories to help debug
const { readdirSync } = require('fs');
log('📂 Directories in current path:');
try {
const files = readdirSync('.');
files.forEach(file => {
const isDir = existsSync(file) && require('fs').statSync(file).isDirectory();
log(`${isDir ? '📁' : '📄'} ${file}`);
});
} catch (err) {
log(`⚠️ Error listing directory: ${err.message}`);
}
} else {
log('✅ Found test-scripts directory');
// Check if generate_data.ts exists
if (existsSync('test-scripts/generate_data.ts')) {
log('✅ Found generate_data.ts');
} else {
log('⚠️ generate_data.ts not found in test-scripts directory');
// List files in test-scripts to help debug
const { readdirSync } = require('fs');
log('📂 Files in test-scripts:');
try {
const files = readdirSync('test-scripts');
files.forEach(file => {
log(`📄 ${file}`);
});
} catch (err) {
log(`⚠️ Error listing test-scripts: ${err.message}`);
}
}
}
// Check directory structure
log('📁 Current directory:', process.cwd());
log('📁 Directory contents:', require('fs').readdirSync('.'));
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true });
}
try {
// Try to generate test data using the script
log('🔄 Running test data generation script...');
log('🔄 Attempting to run generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
log('✅ Test data generation script completed');
log('✅ Test data generation completed');
// Verify the generated files exist
// Verify and log generated files content
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying generated files:');
log('\n📝 Verifying generated files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} was not generated`);
throw new Error(`Required file ${file} was not generated`);
log(`❌ Missing file: ${file}`);
} else {
log(`${file} exists`);
const content = readFileSync(file, 'utf8');
log(`\n📄 Content of ${file}:`);
log(content);
try {
const parsed = JSON.parse(content);
if (file.includes('test-env.json')) {
log('🔑 CONTACT1_DID in test-env:', parsed.CONTACT1_DID);
}
if (file.includes('contacts.json')) {
log('👥 First contact DID:', parsed[0]?.did);
}
} catch (e) {
log(`❌ Error parsing ${file}:`, e);
}
}
}
} catch (error) {
log(`⚠️ Failed to generate test data: ${error.message}`);
log(`\n⚠️ Test data generation failed: ${error.message}`);
log('⚠️ Creating fallback test data...');
// Create minimal fallback test data
// Create fallback data with detailed logging
const fallbackTestEnv = {
"CONTACT1_DID": "did:example:123456789",
"CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B",
"APP_URL": "https://app.timesafari.example"
};
const fallbackClaimDetails = {
"claim_id": "claim_12345",
"title": "Test Claim",
"description": "This is a test claim"
};
const fallbackContacts = [
{
"id": "contact1",
"name": "Test Contact",
"did": "did:example:123456789"
"did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B"
}
];
// Use writeFileSync to overwrite any existing files
const { writeFileSync } = require('fs');
log('\n📝 Writing fallback data:');
log('TestEnv:', JSON.stringify(fallbackTestEnv, null, 2));
log('Contacts:', JSON.stringify(fallbackContacts, null, 2));
writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(fallbackClaimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
log('✅ Fallback test data created');
// Verify files were created
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('🔍 Verifying fallback files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Failed to create ${file}`);
} else {
log(`✅ Created ${file}`);
}
// Verify fallback data was written
log('\n🔍 Verifying fallback data:');
try {
const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
log('Written TestEnv:', writtenTestEnv);
log('Written Contacts:', writtenContacts);
} catch (e) {
log('❌ Error verifying fallback data:', e);
}
}
};
@@ -282,40 +423,45 @@ const buildWebAssets = async (log) => {
// Configure iOS project
const configureIosProject = async (log) => {
log('📱 Syncing Capacitor project...');
try {
execSync('npx cap sync ios', { stdio: 'inherit' });
log('✅ Capacitor sync completed');
} catch (error) {
log('⚠️ Capacitor sync encountered issues. Attempting to continue...');
}
log('📱 Configuring iOS project...');
// Skip cap sync since we just did a clean platform add
log('✅ Using freshly created iOS platform');
// Register URL scheme for deeplink tests
log('🔗 Configuring URL scheme for deeplink tests...');
if (checkAndRegisterUrlScheme(log)) {
log('✅ URL scheme configuration completed');
} else {
log('⚠️ URL scheme could not be registered automatically');
log('⚠️ Deeplink tests may not work correctly');
}
log('⚙️ Installing CocoaPods dependencies...');
try {
// Try to run pod install normally first
log('🔄 Running "pod install" in ios/App directory...');
execSync('cd ios/App && pod install', { stdio: 'inherit' });
log('✅ CocoaPods installation completed');
} catch (error) {
// If that fails, try using sudo (requires password)
log('⚠️ CocoaPods installation failed. Trying with sudo...');
try {
execSync('cd ios/App && sudo pod install', { stdio: 'inherit' });
} catch (sudoError) {
// If both methods fail, alert the user
log('❌ CocoaPods installation failed.');
log('Please run one of the following commands manually:');
log('1. cd ios/App && pod install');
log('2. cd ios/App && sudo pod install');
log('3. Install CocoaPods through Homebrew: brew install cocoapods');
throw new Error('CocoaPods installation failed. See log for details.');
}
// If that fails, provide detailed instructions
log(`⚠️ CocoaPods installation failed: ${error.message}`);
log('⚠️ Please ensure CocoaPods is installed correctly:');
log('1. If using system Ruby: "sudo gem install cocoapods"');
log('2. If using Homebrew Ruby: "brew install cocoapods"');
log('3. Then run: "cd ios/App && pod install"');
// Try to continue despite the error
log('⚠️ Attempting to continue with the build process...');
}
log('✅ CocoaPods installation completed');
// Add information about iOS security dialogs
log('\n📱 iOS Security Dialog Information:');
log('⚠️ iOS will display security confirmation dialogs when testing deeplinks');
log('⚠️ This is a security feature of iOS and cannot be bypassed in normal testing');
log('⚠️ You will need to manually approve each deeplink test by clicking "Open" in the dialog');
log('⚠️ The app must be running in the foreground for deeplinks to work properly');
log('⚠️ If tests appear to hang, check if a security dialog is waiting for your confirmation');
};
// Build and test iOS project
@@ -365,6 +511,96 @@ const runIosApp = async (log, simulator) => {
log('✅ App launched successfully');
};
const validateTestData = (log) => {
log('\n=== VALIDATING TEST DATA ===');
const generateFreshTestData = () => {
log('\n🔄 Generating fresh test data...');
try {
// Ensure .generated directory exists
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
// Execute the generate_data.ts script synchronously
log('Running generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', {
stdio: 'inherit',
encoding: 'utf8'
});
// Read and validate the generated files
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
throw new Error('Generated files not found after running generate_data.ts');
}
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CONTACT1_DID) {
throw new Error('CONTACT1_DID missing from generated test data');
}
log('Generated test data:', {
testEnv: testEnv,
contacts: contacts
});
return { testEnv, contacts };
} catch (error) {
log('❌ Test data generation failed:', error);
throw error;
}
};
try {
// Try to read existing data or generate fresh data
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
let testData;
// If either file is missing or invalid, generate fresh data
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
testData = generateFreshTestData();
} else {
try {
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CLAIM_ID || !testEnv.CONTACT1_DID) {
log('⚠️ Existing test data missing required fields, regenerating...');
testData = generateFreshTestData();
} else {
testData = { testEnv, contacts };
}
} catch (error) {
log('⚠️ Error reading existing test data, regenerating...');
testData = generateFreshTestData();
}
}
// Final validation of data
if (!testData.testEnv.CLAIM_ID || !testData.testEnv.CONTACT1_DID) {
throw new Error('Test data validation failed even after generation');
}
log('✅ Test data validated successfully');
log('📄 Test Environment:', JSON.stringify(testData.testEnv, null, 2));
return testData;
} catch (error) {
log(`❌ Test data validation failed: ${error.message}`);
throw error;
}
};
/**
* Run deeplink tests
* Optionally tests deeplinks if the test data is available
@@ -373,122 +609,115 @@ const runIosApp = async (log, simulator) => {
* @returns {Promise<void>}
*/
const runDeeplinkTests = async (log) => {
log('🔗 Starting deeplink tests...');
log('\n=== Starting Deeplink Tests ===');
// Register URL scheme if needed
checkAndRegisterUrlScheme(log);
// Check if test data files exist first
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} does not exist`);
log('⚠️ Skipping deeplink tests');
return;
}
// Validate test data before proceeding
let testEnv, contacts;
try {
({ testEnv, contacts } = validateTestData(log));
} catch (error) {
log('❌ Cannot proceed with tests due to invalid test data');
log(`Error: ${error.message}`);
log('Please ensure test data is properly generated before running tests');
process.exit(1); // Exit with error code
}
// Now we can safely create the deeplink tests knowing we have valid data
const deeplinkTests = [
{
url: `timesafari://claim/${testEnv.CLAIM_ID}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.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: (() => {
if (!testEnv?.CONTACT1_DID) {
throw new Error('Cannot construct contact-edit URL: CONTACT1_DID is missing');
}
const url = `timesafari://contact-edit/${testEnv.CONTACT1_DID}`;
log('Created contact-edit URL:', url);
return url;
})(),
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// Log the final test configuration
log('\n5. Final Test Configuration:');
deeplinkTests.forEach((test, i) => {
log(`\nTest ${i + 1}:`);
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
});
// Show instructions for iOS security dialogs
log('\n📱 IMPORTANT: iOS Security Dialog Instructions:');
log('1. Each deeplink test will trigger a security confirmation dialog');
log('2. You MUST click "Open" on each dialog to continue testing');
log('3. The app must be running in the FOREGROUND');
log('4. You will need to press Enter in this terminal after handling each dialog');
log('5. You can abort the testing process by pressing Ctrl+C\n');
// Ensure app is in foreground
log('⚠️ IMPORTANT: Please make sure the app is in the FOREGROUND now');
await question('Press Enter when the app is visible and in the foreground...');
try {
// Load test data
log('📂 Loading test data from .generated directory');
let testEnv, claimDetails, contacts;
try {
const testEnvContent = readFileSync('.generated/test-env.json', 'utf8');
testEnv = JSON.parse(testEnvContent);
log('✅ Loaded test-env.json');
} catch (error) {
log(`⚠️ Failed to load test-env.json: ${error.message}`);
return;
}
try {
const claimDetailsContent = readFileSync('.generated/claim_details.json', 'utf8');
claimDetails = JSON.parse(claimDetailsContent);
log('✅ Loaded claim_details.json');
} catch (error) {
log(`⚠️ Failed to load claim_details.json: ${error.message}`);
return;
}
try {
const contactsContent = readFileSync('.generated/contacts.json', 'utf8');
contacts = JSON.parse(contactsContent);
log('✅ Loaded contacts.json');
} catch (error) {
log(`⚠️ Failed to load contacts.json: ${error.message}`);
return;
}
// Check if the app URL scheme is registered in the simulator
log('🔍 Checking if URL scheme is registered in simulator...');
try {
// Attempt to open a simple URL with the scheme
execSync(`xcrun simctl openurl booted "timesafari://test"`, { stdio: 'pipe' });
log('✅ URL scheme is registered and working');
} catch (error) {
const errorMessage = error.message || '';
// Check for the specific error code that indicates an unregistered URL scheme
if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) {
log('⚠️ URL scheme "timesafari://" is not registered in the app or app is not running');
log('⚠️ The scheme was added to Info.plist but the app may need to be rebuilt');
log('⚠️ Trying to continue with tests, but they may fail');
}
}
// 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'
}
];
// Execute each test
let testsCompleted = 0;
let testsSkipped = 0;
for (const test of deeplinkTests) {
// Show upcoming test info before execution
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL to test: ${test.url}`);
log('------------------------');
// Clear prompt for user action
await question('\n⏎ Press Enter to execute this test (or Ctrl+C to quit)...');
try {
log(`\n🔗 Testing deeplink: ${test.description}`);
log(`URL: ${test.url}`);
log('🚀 Executing deeplink test...');
log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' });
log(`✅ Successfully executed: ${test.description}`);
testsCompleted++;
// Wait between tests
await new Promise(resolve => setTimeout(resolve, 5000));
// Show progress
log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// 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('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
} catch (deeplinkError) {
const errorMessage = deeplinkError.message || '';
@@ -501,22 +730,35 @@ const runDeeplinkTests = async (log) => {
log(`⚠️ Error: ${errorMessage}`);
}
log('⚠️ Continuing with next test...');
// Show next test info after error handling
if (testsCompleted + testsSkipped < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted + testsSkipped];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
}
}
log(`✅ Deeplink tests completed: ${testsCompleted} successful, ${testsSkipped} skipped`);
log('\n🎉 All deeplink tests completed!');
log(`✅ Successful: ${testsCompleted}`);
log(`⚠️ Skipped: ${testsSkipped}`);
if (testsSkipped > 0) {
log('\n📝 Note about skipped tests:');
log('1. The app needs to have the URL scheme registered in Info.plist');
log('2. The app needs to be rebuilt after registering the URL scheme');
log('3. The app must be running in the foreground for deeplink tests to work');
log('4. If these conditions are met and tests still fail, check URL handling in the app code');
log('4. iOS security dialogs must be manually approved for each deeplink test');
log('5. If these conditions are met and tests still fail, check URL handling in the app code');
}
} catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Don't rethrow the error to prevent halting the process
}
};
@@ -558,7 +800,7 @@ const checkAndRegisterUrlScheme = (log) => {
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.timesafari.app</string>
<string>app.timesafari</string>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
@@ -585,14 +827,51 @@ const checkAndRegisterUrlScheme = (log) => {
}
};
// Helper function to get the app identifier from package.json or capacitor config
const getAppIdentifier = () => {
try {
// Try to read from capacitor.config.ts/js/json
if (existsSync('capacitor.config.json')) {
const config = JSON.parse(readFileSync('capacitor.config.json', 'utf8'));
return config.appId;
}
if (existsSync('capacitor.config.js')) {
// We can't directly require the file, but we can try to extract the appId
const content = readFileSync('capacitor.config.js', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
if (existsSync('capacitor.config.ts')) {
// Similar approach for TypeScript
const content = readFileSync('capacitor.config.ts', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
// Fall back to package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
if (packageJson.capacitor && packageJson.capacitor.appId) {
return packageJson.capacitor.appId;
}
// Default fallback
return 'app.timesafari';
} catch (error) {
console.error('Error getting app identifier:', error);
return 'app.timesafari'; // Default fallback
}
};
/**
* Runs the complete iOS test suite including build and testing
*
* The function performs the following steps:
* 1. Syncs the Capacitor project with latest web build
* 2. Builds the app using xcodebuild
* 3. Optionally runs tests if configured
* 4. Launches the app in the simulator
* 1. Cleans and resets the iOS platform
* 2. Verifies prerequisites and project setup
* 3. Syncs the Capacitor project with latest web build
* 4. Builds the app using xcodebuild
* 5. Optionally runs tests if configured
* 6. Launches the app in the simulator
*
* If no simulator is running, it automatically selects and boots one.
*
@@ -617,7 +896,16 @@ async function runIosTests() {
try {
log('🚀 Starting iOS build and test process...');
// Generate test data first
// Clean and reset iOS platform first
const cleanSuccess = await cleanIosPlatform(log);
if (!cleanSuccess) {
throw new Error('Failed to clean and reset iOS platform. Please check the logs for details.');
}
// Check prerequisites
await checkPrerequisites(log);
// Generate test data
await generateTestData(log);
// Verify Xcode installation
@@ -626,8 +914,7 @@ async function runIosTests() {
// Check for simulator or boot one if needed
const simulator = await checkSimulator(log);
// Build web assets and configure iOS project
await buildWebAssets(log);
// Configure iOS project
await configureIosProject(log);
// Build and test using the selected simulator