Browse Source

feat(test): Comprehensive iOS test script overhaul with context-aware deeplink testing

* Add complete iOS platform cleanup and reset on each test run
* Implement interactive deeplink testing with keyboard controls between tests
* Add context awareness to verify app is running and in foreground
* Improve error handling and diagnostic messaging throughout
* Auto-register URL schemes and verify app state for reliable testing
Include prerequisites check for Xcode, CocoaPods and simulator availability
* Include prerequisites check for Xcode, CocoaPods and simulator availability
app_id_fix
Matthew Raymer 1 week ago
parent
commit
474999dc9c
  1. 1161
      package-lock.json
  2. 369
      scripts/test-ios.js

1161
package-lock.json

File diff suppressed because it is too large

369
scripts/test-ios.js

@ -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,7 @@
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } = require('fs');
// Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => {
@ -58,6 +60,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...');
@ -282,40 +447,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
@ -375,6 +545,18 @@ const runIosApp = async (log, simulator) => {
const runDeeplinkTests = async (log) => {
log('🔗 Starting deeplink tests...');
// Import readline module for user input
const readline = require('readline');
// Create readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Promisify the question method
const question = (query) => new Promise(resolve => rl.question(query, resolve));
// Register URL scheme if needed
checkAndRegisterUrlScheme(log);
@ -389,10 +571,61 @@ const runDeeplinkTests = async (log) => {
if (!existsSync(file)) {
log(`⚠️ Required file ${file} does not exist`);
log('⚠️ Skipping deeplink tests');
rl.close();
return;
}
}
// Check if our app is actually running in the simulator
log('🔍 Checking if app is currently running in simulator...');
try {
const runningApps = execSync('xcrun simctl listapps booted').toString();
const appIdentifier = getAppIdentifier();
if (!runningApps.includes(appIdentifier)) {
log('⚠️ The app does not appear to be running in the simulator.');
const shouldLaunch = await question('Would you like to launch the app now? (y/n): ');
if (shouldLaunch.toLowerCase() === 'y' || shouldLaunch.toLowerCase() === 'yes') {
// Try launching the app again
log('🚀 Launching app in simulator...');
const simulatorInfo = JSON.parse(execSync('xcrun simctl list -j devices booted').toString());
const booted = Object.values(simulatorInfo.devices)
.flat()
.find(device => device.state === 'Booted');
if (booted) {
execSync(`npx cap run ios --target="${booted.udid}"`, { stdio: 'inherit' });
log('✅ App launched');
} else {
log('⚠️ No booted simulator found');
}
} else {
log('⚠️ Deeplink tests require the app to be running');
log('⚠️ Please launch the app manually and restart the tests');
rl.close();
return;
}
} else {
log('✅ App is running in simulator');
}
} catch (error) {
log(`⚠️ Unable to check if app is running: ${error.message}`);
log('⚠️ Proceeding with deeplink tests, but they may fail if app is not running');
}
// 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');
@ -404,6 +637,7 @@ const runDeeplinkTests = async (log) => {
log('✅ Loaded test-env.json');
} catch (error) {
log(`⚠️ Failed to load test-env.json: ${error.message}`);
rl.close();
return;
}
@ -413,6 +647,7 @@ const runDeeplinkTests = async (log) => {
log('✅ Loaded claim_details.json');
} catch (error) {
log(`⚠️ Failed to load claim_details.json: ${error.message}`);
rl.close();
return;
}
@ -422,6 +657,7 @@ const runDeeplinkTests = async (log) => {
log('✅ Loaded contacts.json');
} catch (error) {
log(`⚠️ Failed to load contacts.json: ${error.message}`);
rl.close();
return;
}
@ -429,6 +665,7 @@ const runDeeplinkTests = async (log) => {
log('🔍 Checking if URL scheme is registered in simulator...');
try {
// Attempt to open a simple URL with the scheme
log('⚠️ A security dialog will appear - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "timesafari://test"`, { stdio: 'pipe' });
log('✅ URL scheme is registered and working');
} catch (error) {
@ -442,6 +679,9 @@ const runDeeplinkTests = async (log) => {
}
}
// Wait for user confirmation before proceeding
await question('Press Enter to continue with the tests...');
// Test URLs
const deeplinkTests = [
{
@ -482,13 +722,16 @@ const runDeeplinkTests = async (log) => {
try {
log(`\n🔗 Testing deeplink: ${test.description}`);
log(`URL: ${test.url}`);
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));
// Wait for user to press Enter before continuing to next test
if (testsCompleted < deeplinkTests.length) {
await question('Press Enter to continue to the next test...');
}
} catch (deeplinkError) {
const errorMessage = deeplinkError.message || '';
@ -501,6 +744,11 @@ const runDeeplinkTests = async (log) => {
log(`⚠️ Error: ${errorMessage}`);
}
log('⚠️ Continuing with next test...');
// Wait for user to press Enter before continuing to next test
if (testsCompleted + testsSkipped < deeplinkTests.length) {
await question('Press Enter to continue to the next test...');
}
}
}
@ -511,11 +759,17 @@ const runDeeplinkTests = async (log) => {
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');
}
// Close readline interface
rl.close();
} catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
// Close readline interface
rl.close();
// Don't rethrow the error to prevent halting the process
}
};
@ -585,14 +839,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.app';
} catch (error) {
console.error('Error getting app identifier:', error);
return 'app.timesafari.app'; // 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 +908,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 +926,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

Loading…
Cancel
Save