@ -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