From c177bfad0d71ae3f9e0c48de1bf2b78b61a517d0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 1 Oct 2025 11:20:57 +1000 Subject: [PATCH 01/15] feat: create simulators pre-loaded with attachments remove reliance on hardcoded env vars no longer push attachments in tests, bloating the devices --- .env.sample | 12 - .github/workflows/android-regression.yml | 12 - .github/workflows/ios-regression.yml | 12 - .gitignore | 3 +- package.json | 2 + .../user_actions_share_to_session.spec.ts | 2 +- run/test/specs/utils/capabilities_ios.ts | 127 ++++--- run/types/DeviceWrapper.ts | 24 +- scripts/cleanup_ios_sims.ts | 114 ++++++ scripts/create_ios_sims.ts | 340 ++++++++++++++++++ 10 files changed, 544 insertions(+), 104 deletions(-) create mode 100644 scripts/cleanup_ios_sims.ts create mode 100644 scripts/create_ios_sims.ts diff --git a/.env.sample b/.env.sample index eeee7ecf7..5681ce4e2 100644 --- a/.env.sample +++ b/.env.sample @@ -7,18 +7,6 @@ AVD_MANAGER_FULL_PATH=/home/yougotthis/Android/Sdk/cmdline-tools/latest/bin/avdm EMULATOR_FULL_PATH=/home/yougotthis/Android/Sdk/emulator/emulator ANDROID_SYSTEM_IMAGE="system-images;android-35;google_atd;x86_64" APPIUM_ADB_FULL_PATH=/home/yougotthis/Android/sdk/platform-tools/adb -IOS_1_SIMULATOR=just_not_empty -IOS_2_SIMULATOR=just_not_empty -IOS_3_SIMULATOR=just_not_empty -IOS_4_SIMULATOR=just_not_empty -IOS_5_SIMULATOR=just_not_empty -IOS_6_SIMULATOR=just_not_empty -IOS_7_SIMULATOR=just_not_empty -IOS_8_SIMULATOR=just_not_empty -IOS_9_SIMULATOR=just_not_empty -IOS_10_SIMULATOR=just_not_empty -IOS_11_SIMULATOR=just_not_empty -IOS_12_SIMULATOR=just_not_empty PRINT_TEST_LOGS=true PRINT_ONGOING_TEST_LOGS = 1 PRINT_FAILED_TEST_LOGS=1 diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 5314b0859..30f9d9dc9 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -71,18 +71,6 @@ jobs: PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} HIDE_WEBDRIVER_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'minimal' && '1' || '0' }} - IOS_1_SIMULATOR: '' - IOS_2_SIMULATOR: '' - IOS_3_SIMULATOR: '' - IOS_4_SIMULATOR: '' - IOS_5_SIMULATOR: '' - IOS_6_SIMULATOR: '' - IOS_7_SIMULATOR: '' - IOS_8_SIMULATOR: '' - IOS_9_SIMULATOR: '' - IOS_10_SIMULATOR: '' - IOS_11_SIMULATOR: '' - IOS_12_SIMULATOR: '' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 85dc2559d..f956326af 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -99,18 +99,6 @@ jobs: AVD_MANAGER_FULL_PATH: '' ANDROID_SYSTEM_IMAGE: '' EMULATOR_FULL_PATH: '' - IOS_1_SIMULATOR: '4A75A0E1-9EDE-4169-93C3-DCE0F0C7664F' - IOS_2_SIMULATOR: 'ACB6A587-8556-4EA0-87CF-4326A9A22051' - IOS_3_SIMULATOR: 'D90B2AE2-FF30-49BE-9370-B789BAEED3BB' - IOS_4_SIMULATOR: '59BD1CA4-7A8D-40FB-BAC7-CC99500644E0' - IOS_5_SIMULATOR: '064F4F80-B81C-4B72-9715-43CD18975139' - IOS_6_SIMULATOR: '56D8BA2F-BA0C-4D8F-8E5B-FD928E2C7C66' - IOS_7_SIMULATOR: '012D6656-D6DE-4932-A460-72F5629EB2E0' - IOS_8_SIMULATOR: 'D66CBD9C-7550-4055-8504-95F0AE700617' - IOS_9_SIMULATOR: '84884861-F8EF-4481-A001-B403F2649FCF' - IOS_10_SIMULATOR: 'C0EE6A21-044D-4B6E-B9A5-7AB977ADF305' - IOS_11_SIMULATOR: 'B8E78B21-1432-41F3-A398-DE4FF8CF9DED' - IOS_12_SIMULATOR: '8214A3A2-D4E1-4AA8-BB0F-394E3A49BCFA' steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index f4b3a32d2..30b583c38 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ avd/* .eslintcache test-results.csv *.csv -/allure* \ No newline at end of file +/allure* +ios-simulators.json \ No newline at end of file diff --git a/package.json b/package.json index a5b797bbb..c36ca3519 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "session-appium", "version": "1.0.0", "scripts": { + "create-simulators": "ts-node ./scripts/create_ios_sims.ts", + "cleanup-simulators": "ts-node ./scripts/cleanup_ios_sims.ts", "lint": "yarn prettier . --write --cache && yarn eslint . --cache ", "lint-check": "yarn prettier . --check && yarn eslint .", "tsc": "tsc", diff --git a/run/test/specs/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts index b53fa5dd4..a7bde2b7e 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -42,7 +42,7 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn await sleepFor(1000); await alice1.pressHome(); await sleepFor(2000); - await alice1.pushMediaToDevice(testImage); + await alice1.onAndroid().pushMediaToDevice(testImage); // iOS is preloaded // Photo app is on different page than Session await alice1.onIOS().swipeRightAny('Session'); await alice1.clickOnElementAll(new PhotoLibrary(alice1)); diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 87bf203bd..354472049 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -1,9 +1,10 @@ import { AppiumXCUITestCapabilities } from '@wdio/types/build/Capabilities'; import { W3CCapabilities } from '@wdio/types/build/Capabilities'; import dotenv from 'dotenv'; +import { existsSync, readFileSync } from 'fs'; -import { IntRange } from '../../../types/RangeType'; dotenv.config(); + const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; if (!iosPathPrefix) { @@ -16,8 +17,8 @@ console.log(`iOS app full path: ${iosAppFullPath}`); const sharediOSCapabilities: AppiumXCUITestCapabilities = { 'appium:app': iosAppFullPath, 'appium:platformName': 'iOS', - 'appium:platformVersion': '17.2', - 'appium:deviceName': 'iPhone 15 Pro Max', + 'appium:platformVersion': '18.3', + 'appium:deviceName': 'iPhone 16 Pro Max', 'appium:automationName': 'XCUITest', 'appium:bundleId': 'com.loki-project.loki-messenger', 'appium:newCommandTimeout': 300000, @@ -31,62 +32,92 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { communityPollLimit: 5, }, }, - // "appium:isHeadless": true, } as AppiumXCUITestCapabilities; -const envVars = [ - 'IOS_1_SIMULATOR', - 'IOS_2_SIMULATOR', - 'IOS_3_SIMULATOR', - 'IOS_4_SIMULATOR', - 'IOS_5_SIMULATOR', - 'IOS_6_SIMULATOR', - 'IOS_7_SIMULATOR', - 'IOS_8_SIMULATOR', - 'IOS_9_SIMULATOR', - 'IOS_10_SIMULATOR', - 'IOS_11_SIMULATOR', - 'IOS_12_SIMULATOR', -] as const; - -function getIOSSimulatorUUIDFromEnv(index: CapabilitiesIndexType): string { - const envVar = envVars[index]; - const uuid = process.env[envVar]; - - if (!uuid) { - throw new Error(`Environment variable ${envVar} is not set`); +type Simulator = { + name: string; + udid: string; + wdaPort: number; + index: number; +}; + +function loadSimulators(): Simulator[] { + const jsonPath = 'ios-simulators.json'; + + // Try JSON first (CI with persistent simulators) + if (existsSync(jsonPath)) { + console.log('šŸ“± Looking for iOS simulators from ios-simulators.json'); + const content = readFileSync(jsonPath, 'utf-8'); + const sims: Simulator[] = JSON.parse(content); + console.log(` Found ${sims.length} simulators`); + return sims; } - return uuid; + // Fallback to environment variables (local dev) + console.log('šŸ“± Looking for iOS simulators from environment variables'); + const envVars = [ + 'IOS_1_SIMULATOR', + 'IOS_2_SIMULATOR', + 'IOS_3_SIMULATOR', + 'IOS_4_SIMULATOR', + 'IOS_5_SIMULATOR', + 'IOS_6_SIMULATOR', + 'IOS_7_SIMULATOR', + 'IOS_8_SIMULATOR', + 'IOS_9_SIMULATOR', + 'IOS_10_SIMULATOR', + 'IOS_11_SIMULATOR', + 'IOS_12_SIMULATOR', + ]; + + const simulators = envVars + .map((envVar, index) => { + const udid = process.env[envVar]; + if (!udid) return null; + + return { + name: `Sim-${index + 1}`, + udid, + wdaPort: 1253 + index, + index, + }; + }) + .filter((sim): sim is Simulator => sim !== null); + + // Re-index to be contiguous + return simulators.map((sim, newIndex) => ({ + ...sim, + wdaPort: 1253 + newIndex, + index: newIndex, + })); } -const MAX_CAPABILITIES_INDEX = envVars.length; - -export type CapabilitiesIndexType = IntRange<0, typeof MAX_CAPABILITIES_INDEX>; -export function capabilityIsValid( - capabilitiesIndex: number -): capabilitiesIndex is CapabilitiesIndexType { - if (capabilitiesIndex < 0 || capabilitiesIndex > MAX_CAPABILITIES_INDEX) { - return false; - } - return true; -} +const simulators = loadSimulators(); -interface CustomW3CCapabilities extends W3CCapabilities { - 'appium:wdaLocalPort': number; - 'appium:udid': string; +if (simulators.length === 0) { + throw new Error( + 'No iOS simulators found.\n' + + 'Run: yarn create-sims 4' + ); } -const emulatorUUIDs = Array.from({ length: MAX_CAPABILITIES_INDEX }, (_, index) => - getIOSSimulatorUUIDFromEnv(index as CapabilitiesIndexType) -); +console.log(`āœ“ Loaded ${simulators.length} iOS simulators`); -const capabilities = emulatorUUIDs.map((udid, index) => ({ +const capabilities = simulators.map(sim => ({ ...sharediOSCapabilities, - 'appium:udid': udid, - 'appium:wdaLocalPort': 1253 + index, + 'appium:udid': sim.udid, + 'appium:wdaLocalPort': sim.wdaPort, })); +export const MAX_CAPABILITIES_INDEX = capabilities.length; +export type CapabilitiesIndexType = number; + +export function capabilityIsValid( + capabilitiesIndex: number +): capabilitiesIndex is CapabilitiesIndexType { + return capabilitiesIndex >= 0 && capabilitiesIndex < MAX_CAPABILITIES_INDEX; +} + export function getIosCapabilities(capabilitiesIndex: CapabilitiesIndexType): W3CCapabilities { if (capabilitiesIndex >= capabilities.length) { throw new Error( @@ -102,11 +133,11 @@ export function getIosCapabilities(capabilitiesIndex: CapabilitiesIndexType): W3 }; } -export function getCapabilitiesForWorker(workerId: number): CustomW3CCapabilities { +export function getCapabilitiesForWorker(workerId: number) { const emulator = capabilities[workerId % capabilities.length]; return { ...sharediOSCapabilities, 'appium:udid': emulator['appium:udid'], 'appium:wdaLocalPort': emulator['appium:wdaLocalPort'], - } as CustomW3CCapabilities; + }; } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 869eab614..c21211cf4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -57,7 +57,6 @@ import { clickOnCoordinates, sleepFor } from '../test/specs/utils'; import { getAdbFullPath } from '../test/specs/utils/binaries'; import { parseDataImage } from '../test/specs/utils/check_colour'; import { isSameColor } from '../test/specs/utils/check_colour'; -import { copyFileToSimulator } from '../test/specs/utils/copy_file_to_simulator'; import { SupportedPlatformsType } from '../test/specs/utils/open_app'; import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/specs/utils/utilities'; import { @@ -1031,7 +1030,6 @@ export class DeviceWrapper { // Iterate over each candidate element for (const el of elements) { - // Take a screenshot of the element const base64 = await this.getElementScreenshot(el.ELEMENT); const elementBuffer = Buffer.from(base64, 'base64'); @@ -1843,9 +1841,8 @@ export class DeviceWrapper { } public async sendImage(message: string, community?: boolean): Promise { + // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { - // Push file first - await this.pushMediaToDevice(testImage); await this.clickOnElementAll(new AttachmentsButton(this)); await sleepFor(5000); const keyboard = await this.isKeyboardVisible(); @@ -1896,11 +1893,8 @@ export class DeviceWrapper { return sentTimestamp; } public async sendVideoiOS(message: string): Promise { - // Push first - await this.pushMediaToDevice(testVideo); + // iOS files are pre-loaded on simulator creation, no need to push await this.clickOnElementAll(new AttachmentsButton(this)); - // Select images button/tab - await sleepFor(5000); const keyboard = await this.isKeyboardVisible(); if (keyboard) { await clickOnCoordinates(this, InteractionPoints.ImagesFolderKeyboardOpen); @@ -1911,12 +1905,8 @@ export class DeviceWrapper { await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access', - maxWait: 2_000, }); await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise - // For some reason video gets added to the top of the Recents folder so it's best to scroll up - await this.scrollUp(); - await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise // A video can't be matched by its thumbnail so we use a video thumbnail file await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, @@ -1991,10 +1981,10 @@ export class DeviceWrapper { } public async sendDocument(): Promise { + // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { const formattedFileName = 'test_file, pdf'; const testMessage = 'Testing documents'; - copyFileToSimulator(this, testFile); await this.clickOnElementAll(new AttachmentsButton(this)); const keyboard = await this.isKeyboardVisible(); if (keyboard) { @@ -2142,9 +2132,8 @@ export class DeviceWrapper { // Click on Profile picture await this.clickOnElementAll(new UserAvatar(this)); await this.clickOnElementAll(new ChangeProfilePictureButton(this)); + // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { - // Push file first - await this.pushMediaToDevice(profilePicture); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately await this.matchAndTapImage( @@ -2452,13 +2441,12 @@ export class DeviceWrapper { // Execute the action in the home screen context const iosPermissions = await this.doesElementExist({ ...args, - maxWait: 1000, + maxWait: 3_000, }); - this.info('iosPermissions', iosPermissions); if (iosPermissions) { await this.clickOnElementAll({ ...args, maxWait }); } else { - this.info('No iosPermissions', iosPermissions); + this.info('No iOS Permissions modal visible to Appium'); } } catch (e) { this.info('FAILED WITH', e); diff --git a/scripts/cleanup_ios_sims.ts b/scripts/cleanup_ios_sims.ts new file mode 100644 index 000000000..3fa2baf82 --- /dev/null +++ b/scripts/cleanup_ios_sims.ts @@ -0,0 +1,114 @@ +import { execSync } from 'child_process'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; + +type Simulator = { + name: string; + udid: string; + wdaPort: number; + index: number; +}; + +function cleanupIOSSimulators() { + const jsonPath = 'ios-simulators.json'; + const envPath = '.env'; + + console.log('\n========================================'); + console.log('Cleaning up iOS simulators'); + console.log('========================================\n'); + + // Check for JSON file (CI) + if (existsSync(jsonPath)) { + const simulators: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); + + console.log(`Found ${simulators.length} simulators in ios-simulators.json\n`); + + for (const sim of simulators) { + try { + console.log(`[${sim.index}] Deleting: ${sim.name}`); + + // Try to shutdown first (ignore errors if already shutdown) + try { + execSync(`xcrun simctl shutdown ${sim.udid}`, { stdio: 'pipe' }); + } catch { + // Already shutdown, that's fine + } + + // Delete the simulator + execSync(`xcrun simctl delete ${sim.udid}`, { stdio: 'pipe' }); + console.log(` āœ“ Deleted ${sim.udid}\n`); + } catch (error) { + console.warn(` ⚠ Failed to delete simulator ${sim.udid}`); + } + } + + // Remove the JSON file + unlinkSync(jsonPath); + console.log('āœ“ Removed ios-simulators.json\n'); + } + + // Check for simulators in .env (local) + if (existsSync(envPath)) { + const envContent = readFileSync(envPath, 'utf-8'); + const simulatorLines = envContent + .split('\n') + .filter(line => line.trim().startsWith('IOS_') && line.includes('_SIMULATOR=')); + + if (simulatorLines.length > 0) { + console.log(`Found ${simulatorLines.length} simulator UDIDs in .env\n`); + + const udids = simulatorLines + .map(line => { + const match = line.match(/IOS_\d+_SIMULATOR=(.+)/); + return match ? match[1].trim() : null; + }) + .filter((udid): udid is string => udid !== null); + + for (const udid of udids) { + try { + console.log(`Deleting: ${udid}`); + + // Try to shutdown first + try { + execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); + } catch { + // Already shutdown + } + + // Delete the simulator + execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); + console.log(` āœ“ Deleted\n`); + } catch (error) { + console.warn(` ⚠ Failed to delete ${udid}\n`); + } + } + + // Remove simulator lines from .env + const cleanedEnv = + envContent + .split('\n') + .filter(line => { + const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); + const isSimComment = line.trim().startsWith('# iOS Simulators'); + return !isSimLine && !isSimComment; + }) + .join('\n') + .trim() + '\n'; + + writeFileSync(envPath, cleanedEnv); + console.log('āœ“ Removed simulator UDIDs from .env\n'); + } + } + + if ( + !existsSync(jsonPath) && + (!existsSync(envPath) || !readFileSync(envPath, 'utf-8').includes('IOS_')) + ) { + console.log('No simulators found to clean up\n'); + } + + console.log('========================================'); + console.log('Cleanup complete'); + console.log('========================================\n'); +} + +cleanupIOSSimulators(); diff --git a/scripts/create_ios_sims.ts b/scripts/create_ios_sims.ts new file mode 100644 index 000000000..205691d60 --- /dev/null +++ b/scripts/create_ios_sims.ts @@ -0,0 +1,340 @@ +import { execSync } from 'child_process'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import * as path from 'path'; + +import type { DeviceWrapper } from '../run/types/DeviceWrapper'; + +import { copyFileToSimulator } from '../run/test/specs/utils/copy_file_to_simulator'; +import { isSimulatorBooted } from './ios_shared'; +import { sleepSync } from './shared'; + +interface SimulatorConfig { + deviceType: string; + runtime: string; + totalSimulators: number; +} + +interface Simulator { + name: string; + udid: string; + wdaPort: number; + index: number; +} + +const MEDIA_ROOT = path.join('run', 'test', 'specs', 'media'); + +const MEDIA_FILES = { + images: ['profile_picture.jpg', 'test_image.jpg'], + videos: ['test_video.mp4'], + pdfs: ['test_file.pdf'], +}; + +function createSimulator(name: string, deviceType: string, runtime: string): string { + console.log(`Creating simulator: ${name}`); + + const output = execSync(`xcrun simctl create "${name}" "${deviceType}" "${runtime}"`, { + encoding: 'utf-8', + }).trim(); + + console.log(` Created with UDID: ${output}`); + return output; +} + +function cloneSimulator(sourceUdid: string, newName: string): string { + console.log(`Cloning simulator: ${newName}`); + + const output = execSync(`xcrun simctl clone ${sourceUdid} "${newName}"`, { + encoding: 'utf-8', + }).trim(); + + console.log(` Cloned with UDID: ${output}`); + return output; +} + +function bootSimulator(udid: string, index: number): boolean { + try { + console.log(`Booting simulator ${index}: ${udid}`); + execSync(`xcrun simctl boot ${udid}`, { stdio: 'inherit' }); + } catch (error: any) { + if (error.message?.includes('Unable to boot device in current state: Booted')) { + console.log(` Simulator already booted`); + return true; + } + console.error(`Error: Boot command failed for ${udid}`); + console.error(error.stderr?.toString() || error.message); + return false; + } + + return true; +} + +function shutdownSimulator(udid: string): void { + console.log('Shutting down simulator...'); + try { + execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); + console.log(' āœ“ Shutdown complete'); + } catch (error) { + console.warn(' ⚠ Warning: Failed to shutdown (may already be shutdown)'); + } +} + +function waitForBoot(udid: string): boolean { + console.log(` Waiting for boot to complete...`); + + sleepSync(2); + + for (let i = 0; i < 30; i++) { + if (isSimulatorBooted(udid)) { + console.log(` āœ“ Boot complete`); + return true; + } + sleepSync(1); + } + + console.error(` āœ— Simulator did not boot within 30 seconds`); + return false; +} + +function preloadImagesAndVideos(udid: string): void { + console.log(`Preloading images and videos...`); + + const allMediaFiles = [...MEDIA_FILES.images, ...MEDIA_FILES.videos]; + + for (const filename of allMediaFiles) { + const mediaPath = path.join(MEDIA_ROOT, filename); + + if (!existsSync(mediaPath)) { + console.warn(` ⚠ Warning: Media file not found: ${mediaPath}`); + continue; + } + + try { + execSync(`xcrun simctl addmedia ${udid} "${mediaPath}"`, { stdio: 'pipe' }); + console.log(` āœ“ Added ${filename}`); + } catch (error) { + console.error(` āœ— Failed to add ${filename}:`, error); + throw error; + } + } +} + +function preloadPDFs(udid: string): void { + console.log(`Preloading PDFs...`); + + const mockDevice: Pick = { + udid, + log: (message: string) => console.log(` ${message}`), + }; + + for (const filename of MEDIA_FILES.pdfs) { + const sourcePath = path.join(MEDIA_ROOT, filename); + + if (!existsSync(sourcePath)) { + console.warn(` ⚠ Warning: PDF file not found: ${sourcePath}`); + continue; + } + + try { + copyFileToSimulator(mockDevice as DeviceWrapper, filename); + console.log(` āœ“ Copied ${filename} to Downloads`); + } catch (error) { + console.error(` āœ— Failed to copy ${filename}:`, error); + throw error; + } + } +} + +function createMasterSimulatorWithMedia(config: SimulatorConfig): string { + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('Creating master simulator with preloaded media'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const masterName = `Automation-Master-${Date.now()}`; + const masterUdid = createSimulator(masterName, config.deviceType, config.runtime); + + const bootSuccess = bootSimulator(masterUdid, 0); + if (!bootSuccess) { + throw new Error('Failed to boot master simulator'); + } + + const bootComplete = waitForBoot(masterUdid); + if (!bootComplete) { + throw new Error('Master simulator did not boot in time'); + } + + preloadImagesAndVideos(masterUdid); + preloadPDFs(masterUdid); + + shutdownSimulator(masterUdid); + + console.log(`āœ“ Master simulator ready: ${masterName} (${masterUdid})\n`); + + return masterUdid; +} + +function updateLocalEnvFile(simulators: Simulator[]): void { + const envPath = '.env'; + let envContent = ''; + + // Read existing .env if it exists + if (existsSync(envPath)) { + envContent = readFileSync(envPath, 'utf-8'); + + // Remove old IOS_X_SIMULATOR lines + envContent = envContent + .split('\n') + .filter(line => { + const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); + return !isSimLine; + }) + .join('\n'); + } + + // Add new simulator UDIDs + const simLines = simulators.map((sim, i) => `IOS_${i + 1}_SIMULATOR=${sim.udid}`).join('\n'); + + envContent = envContent.trim() + '\n\n# iOS Simulators (auto-generated)\n' + simLines + '\n'; + + writeFileSync(envPath, envContent); + console.log(`āœ“ Updated .env with ${simulators.length} simulator UDIDs`); +} + +function saveSimulatorConfig(simulators: Simulator[]): void { + if (process.env.CI) { + // CI: Save to persistent JSON file + console.log('\nšŸ“ CI environment detected - saving to ios-simulators.json'); + const outputPath = 'ios-simulators.json'; + writeFileSync(outputPath, JSON.stringify(simulators, null, 2)); + console.log(`āœ“ Configuration saved to ${outputPath} (persistent)`); + } else { + // Local: Update .env file + console.log('\nšŸ“ Local environment detected - updating .env file'); + updateLocalEnvFile(simulators); + } +} + +function createIOSSimulators(config: SimulatorConfig) { + const startTime = Date.now(); + const simulators: Simulator[] = []; + + console.log('\n========================================'); + console.log('iOS Simulator Setup - Clone Method'); + console.log('========================================'); + console.log(`Creating ${config.totalSimulators} iOS simulators`); + console.log(` Device type: ${config.deviceType}`); + console.log(` Runtime: ${config.runtime}`); + console.log(` Strategy: Create 1 master + clone ${config.totalSimulators - 1} times`); + console.log('========================================\n'); + + // Verify media files + console.log('Verifying media files...'); + const allFiles = [...MEDIA_FILES.images, ...MEDIA_FILES.videos, ...MEDIA_FILES.pdfs]; + let missingFiles = 0; + + for (const filename of allFiles) { + const mediaPath = path.join(MEDIA_ROOT, filename); + if (existsSync(mediaPath)) { + console.log(` āœ“ ${filename}`); + } else { + console.error(` āœ— ${filename} - NOT FOUND at ${mediaPath}`); + missingFiles++; + } + } + + if (missingFiles > 0) { + console.warn(`\n⚠ Warning: ${missingFiles} media file(s) missing`); + console.log('Continuing anyway... (will skip missing files)\n'); + } else { + console.log('\nāœ“ All media files verified\n'); + } + + const setupStartTime = Date.now(); + + // Step 1: Create master simulator with all media + const masterUdid = createMasterSimulatorWithMedia(config); + + const masterCompleteTime = Date.now(); + console.log( + `ā±ļø Master creation time: ${((masterCompleteTime - setupStartTime) / 1000).toFixed(2)}s\n` + ); + + // Step 2: Clone the master for all needed simulators + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(`Cloning master simulator ${config.totalSimulators} times`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const cloneStartTime = Date.now(); + + for (let index = 0; index < config.totalSimulators; index++) { + console.log(`[${index + 1}/${config.totalSimulators}] Simulator ${index}`); + + const timestamp = Date.now(); + const name = `Automation-${index}-${timestamp}`; + const udid = cloneSimulator(masterUdid, name); + + simulators.push({ + name, + udid, + wdaPort: 1253 + index, + index, + }); + + console.log(`āœ“ Cloned: ${name} (${udid})\n`); + } + + const cloneCompleteTime = Date.now(); + console.log(`ā±ļø Cloning time: ${((cloneCompleteTime - cloneStartTime) / 1000).toFixed(2)}s\n`); + + // Step 3: Delete the master simulator + console.log('Cleaning up master simulator...'); + try { + execSync(`xcrun simctl delete ${masterUdid}`, { stdio: 'pipe' }); + console.log(`āœ“ Master simulator deleted\n`); + } catch (error) { + console.warn(`⚠ Warning: Failed to delete master simulator ${masterUdid}`); + } + + const endTime = Date.now(); + const totalTime = ((endTime - startTime) / 1000).toFixed(2); + + console.log('========================================'); + console.log(`āœ“ All ${simulators.length} simulators created`); + console.log(`ā±ļø TOTAL TIME: ${totalTime}s`); + console.log('========================================\n'); + + console.log('Summary:'); + simulators.forEach(sim => { + console.log(` [${sim.index}] ${sim.name} - ${sim.udid}`); + }); + console.log(''); + + // Save configuration based on environment + saveSimulatorConfig(simulators); + + return simulators; +} + +// Main execution +// Support both env var and command line arg +const numSimulatorsArg = process.argv[2]; +const numSimulators = numSimulatorsArg + ? parseInt(numSimulatorsArg) + : parseInt(process.env.NUM_SIMULATORS || '12'); + +if (isNaN(numSimulators) || numSimulators < 1) { + console.error('āŒ Invalid number of simulators'); + console.error('Usage: yarn create-simulators '); + process.exit(1); +} + +try { + createIOSSimulators({ + deviceType: 'iPhone 16 Pro Max', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-3', + totalSimulators: numSimulators, + }); +} catch (error) { + console.error('\nāŒ Failed to create iOS simulators'); + console.error(error); + process.exit(1); +} From 84f7cec13f4ca44f0c65fcce87c8cc5bc58fbc73 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 1 Oct 2025 13:48:56 +1000 Subject: [PATCH 02/15] fix: tidy up scripts --- package.json | 4 +- run/test/specs/utils/capabilities_ios.ts | 5 +- scripts/cleanup_ios_sims.ts | 114 -------- scripts/cleanup_ios_simulators.ts | 120 ++++++++ scripts/create_ios_sims.ts | 340 ----------------------- scripts/create_ios_simulators.ts | 248 +++++++++++++++++ 6 files changed, 371 insertions(+), 460 deletions(-) delete mode 100644 scripts/cleanup_ios_sims.ts create mode 100644 scripts/cleanup_ios_simulators.ts delete mode 100644 scripts/create_ios_sims.ts create mode 100644 scripts/create_ios_simulators.ts diff --git a/package.json b/package.json index c36ca3519..45eb54c94 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "session-appium", "version": "1.0.0", "scripts": { - "create-simulators": "ts-node ./scripts/create_ios_sims.ts", - "cleanup-simulators": "ts-node ./scripts/cleanup_ios_sims.ts", + "cleanup-simulators": "npx ts-node scripts/cleanup_ios_simulators.ts", + "create-simulators": "yarn cleanup-simulators && npx ts-node scripts/create_ios_simulators.ts", "lint": "yarn prettier . --write --cache && yarn eslint . --cache ", "lint-check": "yarn prettier . --check && yarn eslint .", "tsc": "tsc", diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 354472049..b19e8cd4f 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -95,10 +95,7 @@ function loadSimulators(): Simulator[] { const simulators = loadSimulators(); if (simulators.length === 0) { - throw new Error( - 'No iOS simulators found.\n' + - 'Run: yarn create-sims 4' - ); + throw new Error('No iOS Simulators found.\n' + 'Run: yarn create-simulators '); } console.log(`āœ“ Loaded ${simulators.length} iOS simulators`); diff --git a/scripts/cleanup_ios_sims.ts b/scripts/cleanup_ios_sims.ts deleted file mode 100644 index 3fa2baf82..000000000 --- a/scripts/cleanup_ios_sims.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; - -type Simulator = { - name: string; - udid: string; - wdaPort: number; - index: number; -}; - -function cleanupIOSSimulators() { - const jsonPath = 'ios-simulators.json'; - const envPath = '.env'; - - console.log('\n========================================'); - console.log('Cleaning up iOS simulators'); - console.log('========================================\n'); - - // Check for JSON file (CI) - if (existsSync(jsonPath)) { - const simulators: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); - - console.log(`Found ${simulators.length} simulators in ios-simulators.json\n`); - - for (const sim of simulators) { - try { - console.log(`[${sim.index}] Deleting: ${sim.name}`); - - // Try to shutdown first (ignore errors if already shutdown) - try { - execSync(`xcrun simctl shutdown ${sim.udid}`, { stdio: 'pipe' }); - } catch { - // Already shutdown, that's fine - } - - // Delete the simulator - execSync(`xcrun simctl delete ${sim.udid}`, { stdio: 'pipe' }); - console.log(` āœ“ Deleted ${sim.udid}\n`); - } catch (error) { - console.warn(` ⚠ Failed to delete simulator ${sim.udid}`); - } - } - - // Remove the JSON file - unlinkSync(jsonPath); - console.log('āœ“ Removed ios-simulators.json\n'); - } - - // Check for simulators in .env (local) - if (existsSync(envPath)) { - const envContent = readFileSync(envPath, 'utf-8'); - const simulatorLines = envContent - .split('\n') - .filter(line => line.trim().startsWith('IOS_') && line.includes('_SIMULATOR=')); - - if (simulatorLines.length > 0) { - console.log(`Found ${simulatorLines.length} simulator UDIDs in .env\n`); - - const udids = simulatorLines - .map(line => { - const match = line.match(/IOS_\d+_SIMULATOR=(.+)/); - return match ? match[1].trim() : null; - }) - .filter((udid): udid is string => udid !== null); - - for (const udid of udids) { - try { - console.log(`Deleting: ${udid}`); - - // Try to shutdown first - try { - execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); - } catch { - // Already shutdown - } - - // Delete the simulator - execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); - console.log(` āœ“ Deleted\n`); - } catch (error) { - console.warn(` ⚠ Failed to delete ${udid}\n`); - } - } - - // Remove simulator lines from .env - const cleanedEnv = - envContent - .split('\n') - .filter(line => { - const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); - const isSimComment = line.trim().startsWith('# iOS Simulators'); - return !isSimLine && !isSimComment; - }) - .join('\n') - .trim() + '\n'; - - writeFileSync(envPath, cleanedEnv); - console.log('āœ“ Removed simulator UDIDs from .env\n'); - } - } - - if ( - !existsSync(jsonPath) && - (!existsSync(envPath) || !readFileSync(envPath, 'utf-8').includes('IOS_')) - ) { - console.log('No simulators found to clean up\n'); - } - - console.log('========================================'); - console.log('Cleanup complete'); - console.log('========================================\n'); -} - -cleanupIOSSimulators(); diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts new file mode 100644 index 000000000..81e405d9f --- /dev/null +++ b/scripts/cleanup_ios_simulators.ts @@ -0,0 +1,120 @@ +import { execSync } from 'child_process'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; + +import type { Simulator } from './create_ios_simulators'; + +/** + * iOS Simulator Cleanup Script + * + * Deletes iOS Simulators created by create_ios_simulators.ts and cleans up configuration files. + * + * Environment-specific behavior: + * - Local dev: Deletes Simulators listed in .env and removes those entries + * - CI: Deletes Simulators listed in ios-simulators.json and removes the file + * + * Usage: + * yarn cleanup-simulators + */ + +function deleteSimulator(udid: string): boolean { + try { + execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); + } catch { + // Already shutdown + } + + try { + execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +function cleanupFromJSON(): number { + const jsonPath = 'ios-simulators.json'; + + if (!existsSync(jsonPath)) { + return 0; + } + + const simulators: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); + let deleted = 0; + + for (const sim of simulators) { + if (deleteSimulator(sim.udid)) { + deleted++; + } else { + console.warn(`Failed to delete: ${sim.udid}`); + } + } + + unlinkSync(jsonPath); + console.log(`āœ“ Removed ios-simulators.json`); + + return deleted; +} + +function cleanupFromEnv(): number { + const envPath = '.env'; + + if (!existsSync(envPath)) { + return 0; + } + + const envContent = readFileSync(envPath, 'utf-8'); + const lines = envContent.split('\n'); + + const udids = lines + .filter(line => line.trim().startsWith('IOS_') && line.includes('_SIMULATOR=')) + .map(line => { + const match = line.match(/IOS_\d+_SIMULATOR=(.+)/); + return match ? match[1].trim() : null; + }) + .filter((udid): udid is string => udid !== null); + + if (udids.length === 0) { + return 0; + } + + let deleted = 0; + for (const udid of udids) { + if (deleteSimulator(udid)) { + deleted++; + } else { + console.warn(`Failed to delete: ${udid}`); + } + } + + // Remove simulator lines from .env + const cleanedEnv = + lines + .filter(line => { + const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); + const isSimComment = line.trim().startsWith('# iOS Simulators'); + return !isSimLine && !isSimComment; + }) + .join('\n') + .trim() + '\n'; + + writeFileSync(envPath, cleanedEnv); + console.log(`āœ“ Cleaned .env`); + + return deleted; +} + +function cleanupIOSSimulators(): void { + console.log('\nCleaning up iOS Simulators...\n'); + + const deletedFromJSON = cleanupFromJSON(); + const deletedFromEnv = cleanupFromEnv(); + const totalDeleted = deletedFromJSON + deletedFromEnv; + + if (totalDeleted === 0) { + console.log('No Simulators found to clean up'); + } else { + console.log(`\nāœ“ Deleted ${totalDeleted} Simulator${totalDeleted !== 1 ? 's' : ''}`); + } +} + +cleanupIOSSimulators(); diff --git a/scripts/create_ios_sims.ts b/scripts/create_ios_sims.ts deleted file mode 100644 index 205691d60..000000000 --- a/scripts/create_ios_sims.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import * as path from 'path'; - -import type { DeviceWrapper } from '../run/types/DeviceWrapper'; - -import { copyFileToSimulator } from '../run/test/specs/utils/copy_file_to_simulator'; -import { isSimulatorBooted } from './ios_shared'; -import { sleepSync } from './shared'; - -interface SimulatorConfig { - deviceType: string; - runtime: string; - totalSimulators: number; -} - -interface Simulator { - name: string; - udid: string; - wdaPort: number; - index: number; -} - -const MEDIA_ROOT = path.join('run', 'test', 'specs', 'media'); - -const MEDIA_FILES = { - images: ['profile_picture.jpg', 'test_image.jpg'], - videos: ['test_video.mp4'], - pdfs: ['test_file.pdf'], -}; - -function createSimulator(name: string, deviceType: string, runtime: string): string { - console.log(`Creating simulator: ${name}`); - - const output = execSync(`xcrun simctl create "${name}" "${deviceType}" "${runtime}"`, { - encoding: 'utf-8', - }).trim(); - - console.log(` Created with UDID: ${output}`); - return output; -} - -function cloneSimulator(sourceUdid: string, newName: string): string { - console.log(`Cloning simulator: ${newName}`); - - const output = execSync(`xcrun simctl clone ${sourceUdid} "${newName}"`, { - encoding: 'utf-8', - }).trim(); - - console.log(` Cloned with UDID: ${output}`); - return output; -} - -function bootSimulator(udid: string, index: number): boolean { - try { - console.log(`Booting simulator ${index}: ${udid}`); - execSync(`xcrun simctl boot ${udid}`, { stdio: 'inherit' }); - } catch (error: any) { - if (error.message?.includes('Unable to boot device in current state: Booted')) { - console.log(` Simulator already booted`); - return true; - } - console.error(`Error: Boot command failed for ${udid}`); - console.error(error.stderr?.toString() || error.message); - return false; - } - - return true; -} - -function shutdownSimulator(udid: string): void { - console.log('Shutting down simulator...'); - try { - execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); - console.log(' āœ“ Shutdown complete'); - } catch (error) { - console.warn(' ⚠ Warning: Failed to shutdown (may already be shutdown)'); - } -} - -function waitForBoot(udid: string): boolean { - console.log(` Waiting for boot to complete...`); - - sleepSync(2); - - for (let i = 0; i < 30; i++) { - if (isSimulatorBooted(udid)) { - console.log(` āœ“ Boot complete`); - return true; - } - sleepSync(1); - } - - console.error(` āœ— Simulator did not boot within 30 seconds`); - return false; -} - -function preloadImagesAndVideos(udid: string): void { - console.log(`Preloading images and videos...`); - - const allMediaFiles = [...MEDIA_FILES.images, ...MEDIA_FILES.videos]; - - for (const filename of allMediaFiles) { - const mediaPath = path.join(MEDIA_ROOT, filename); - - if (!existsSync(mediaPath)) { - console.warn(` ⚠ Warning: Media file not found: ${mediaPath}`); - continue; - } - - try { - execSync(`xcrun simctl addmedia ${udid} "${mediaPath}"`, { stdio: 'pipe' }); - console.log(` āœ“ Added ${filename}`); - } catch (error) { - console.error(` āœ— Failed to add ${filename}:`, error); - throw error; - } - } -} - -function preloadPDFs(udid: string): void { - console.log(`Preloading PDFs...`); - - const mockDevice: Pick = { - udid, - log: (message: string) => console.log(` ${message}`), - }; - - for (const filename of MEDIA_FILES.pdfs) { - const sourcePath = path.join(MEDIA_ROOT, filename); - - if (!existsSync(sourcePath)) { - console.warn(` ⚠ Warning: PDF file not found: ${sourcePath}`); - continue; - } - - try { - copyFileToSimulator(mockDevice as DeviceWrapper, filename); - console.log(` āœ“ Copied ${filename} to Downloads`); - } catch (error) { - console.error(` āœ— Failed to copy ${filename}:`, error); - throw error; - } - } -} - -function createMasterSimulatorWithMedia(config: SimulatorConfig): string { - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log('Creating master simulator with preloaded media'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const masterName = `Automation-Master-${Date.now()}`; - const masterUdid = createSimulator(masterName, config.deviceType, config.runtime); - - const bootSuccess = bootSimulator(masterUdid, 0); - if (!bootSuccess) { - throw new Error('Failed to boot master simulator'); - } - - const bootComplete = waitForBoot(masterUdid); - if (!bootComplete) { - throw new Error('Master simulator did not boot in time'); - } - - preloadImagesAndVideos(masterUdid); - preloadPDFs(masterUdid); - - shutdownSimulator(masterUdid); - - console.log(`āœ“ Master simulator ready: ${masterName} (${masterUdid})\n`); - - return masterUdid; -} - -function updateLocalEnvFile(simulators: Simulator[]): void { - const envPath = '.env'; - let envContent = ''; - - // Read existing .env if it exists - if (existsSync(envPath)) { - envContent = readFileSync(envPath, 'utf-8'); - - // Remove old IOS_X_SIMULATOR lines - envContent = envContent - .split('\n') - .filter(line => { - const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); - return !isSimLine; - }) - .join('\n'); - } - - // Add new simulator UDIDs - const simLines = simulators.map((sim, i) => `IOS_${i + 1}_SIMULATOR=${sim.udid}`).join('\n'); - - envContent = envContent.trim() + '\n\n# iOS Simulators (auto-generated)\n' + simLines + '\n'; - - writeFileSync(envPath, envContent); - console.log(`āœ“ Updated .env with ${simulators.length} simulator UDIDs`); -} - -function saveSimulatorConfig(simulators: Simulator[]): void { - if (process.env.CI) { - // CI: Save to persistent JSON file - console.log('\nšŸ“ CI environment detected - saving to ios-simulators.json'); - const outputPath = 'ios-simulators.json'; - writeFileSync(outputPath, JSON.stringify(simulators, null, 2)); - console.log(`āœ“ Configuration saved to ${outputPath} (persistent)`); - } else { - // Local: Update .env file - console.log('\nšŸ“ Local environment detected - updating .env file'); - updateLocalEnvFile(simulators); - } -} - -function createIOSSimulators(config: SimulatorConfig) { - const startTime = Date.now(); - const simulators: Simulator[] = []; - - console.log('\n========================================'); - console.log('iOS Simulator Setup - Clone Method'); - console.log('========================================'); - console.log(`Creating ${config.totalSimulators} iOS simulators`); - console.log(` Device type: ${config.deviceType}`); - console.log(` Runtime: ${config.runtime}`); - console.log(` Strategy: Create 1 master + clone ${config.totalSimulators - 1} times`); - console.log('========================================\n'); - - // Verify media files - console.log('Verifying media files...'); - const allFiles = [...MEDIA_FILES.images, ...MEDIA_FILES.videos, ...MEDIA_FILES.pdfs]; - let missingFiles = 0; - - for (const filename of allFiles) { - const mediaPath = path.join(MEDIA_ROOT, filename); - if (existsSync(mediaPath)) { - console.log(` āœ“ ${filename}`); - } else { - console.error(` āœ— ${filename} - NOT FOUND at ${mediaPath}`); - missingFiles++; - } - } - - if (missingFiles > 0) { - console.warn(`\n⚠ Warning: ${missingFiles} media file(s) missing`); - console.log('Continuing anyway... (will skip missing files)\n'); - } else { - console.log('\nāœ“ All media files verified\n'); - } - - const setupStartTime = Date.now(); - - // Step 1: Create master simulator with all media - const masterUdid = createMasterSimulatorWithMedia(config); - - const masterCompleteTime = Date.now(); - console.log( - `ā±ļø Master creation time: ${((masterCompleteTime - setupStartTime) / 1000).toFixed(2)}s\n` - ); - - // Step 2: Clone the master for all needed simulators - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(`Cloning master simulator ${config.totalSimulators} times`); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const cloneStartTime = Date.now(); - - for (let index = 0; index < config.totalSimulators; index++) { - console.log(`[${index + 1}/${config.totalSimulators}] Simulator ${index}`); - - const timestamp = Date.now(); - const name = `Automation-${index}-${timestamp}`; - const udid = cloneSimulator(masterUdid, name); - - simulators.push({ - name, - udid, - wdaPort: 1253 + index, - index, - }); - - console.log(`āœ“ Cloned: ${name} (${udid})\n`); - } - - const cloneCompleteTime = Date.now(); - console.log(`ā±ļø Cloning time: ${((cloneCompleteTime - cloneStartTime) / 1000).toFixed(2)}s\n`); - - // Step 3: Delete the master simulator - console.log('Cleaning up master simulator...'); - try { - execSync(`xcrun simctl delete ${masterUdid}`, { stdio: 'pipe' }); - console.log(`āœ“ Master simulator deleted\n`); - } catch (error) { - console.warn(`⚠ Warning: Failed to delete master simulator ${masterUdid}`); - } - - const endTime = Date.now(); - const totalTime = ((endTime - startTime) / 1000).toFixed(2); - - console.log('========================================'); - console.log(`āœ“ All ${simulators.length} simulators created`); - console.log(`ā±ļø TOTAL TIME: ${totalTime}s`); - console.log('========================================\n'); - - console.log('Summary:'); - simulators.forEach(sim => { - console.log(` [${sim.index}] ${sim.name} - ${sim.udid}`); - }); - console.log(''); - - // Save configuration based on environment - saveSimulatorConfig(simulators); - - return simulators; -} - -// Main execution -// Support both env var and command line arg -const numSimulatorsArg = process.argv[2]; -const numSimulators = numSimulatorsArg - ? parseInt(numSimulatorsArg) - : parseInt(process.env.NUM_SIMULATORS || '12'); - -if (isNaN(numSimulators) || numSimulators < 1) { - console.error('āŒ Invalid number of simulators'); - console.error('Usage: yarn create-simulators '); - process.exit(1); -} - -try { - createIOSSimulators({ - deviceType: 'iPhone 16 Pro Max', - runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-3', - totalSimulators: numSimulators, - }); -} catch (error) { - console.error('\nāŒ Failed to create iOS simulators'); - console.error(error); - process.exit(1); -} diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts new file mode 100644 index 000000000..39040093d --- /dev/null +++ b/scripts/create_ios_simulators.ts @@ -0,0 +1,248 @@ +import { execSync } from 'child_process'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import * as path from 'path'; + +import type { DeviceWrapper } from '../run/types/DeviceWrapper'; + +import { copyFileToSimulator } from '../run/test/specs/utils/copy_file_to_simulator'; +import { isSimulatorBooted } from './ios_shared'; +import { sleepSync } from './shared'; + +/** + * iOS Simulator Creation Script + * + * Creates iOS Simulators with preloaded media (images, videos, PDFs) using a clone-based approach: + * 1. Creates one "template" Simulator (Simulator 0) + * 2. Boots it, loads media, then shuts it down + * 3. Clones it N-1 times to create remaining Simulators + * + * Note: You can't add media to shutdown Simulators and you can't clone booted Simulators, + * which is why we boot -> load -> shutdown -> clone. + * + * Environment-specific behavior: + * - Local dev: Updates .env with IOS_N_SIMULATOR variables + * - CI: Creates ios-simulators.json for persistent Simulator tracking + * + * Usage: + * yarn create-simulators 4 # Local: 4 Simulators + * yarn create-simulators 12 # CI: 12 Simulators + */ + +type SimulatorConfig = { + deviceType: string; + runtime: string; + totalSimulators: number; +}; + +export type Simulator = { + name: string; + udid: string; + wdaPort: number; + index: number; +}; +// Define the device type and runtime to create +const DEVICE_CONFIG = { + type: 'iPhone 16 Pro Max', + name: '16PM', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-3', // xcrun simctl list runtimes +}; + +const MEDIA_ROOT = path.join('run', 'test', 'specs', 'media'); +const MEDIA_FILES = { + images: ['profile_picture.jpg', 'test_image.jpg'], + videos: ['test_video.mp4'], + pdfs: ['test_file.pdf'], +}; + +function createSimulator(name: string, deviceType: string, runtime: string): string { + const output = execSync(`xcrun simctl create "${name}" "${deviceType}" "${runtime}"`, { + encoding: 'utf-8', + }).trim(); + return output; +} + +function cloneSimulator(sourceUdid: string, newName: string): string { + const output = execSync(`xcrun simctl clone ${sourceUdid} "${newName}"`, { + encoding: 'utf-8', + }).trim(); + return output; +} + +function bootSimulator(udid: string): boolean { + try { + execSync(`xcrun simctl boot ${udid}`, { stdio: 'pipe' }); + return true; + } catch (error: any) { + if (error.message?.includes('Unable to boot device in current state: Booted')) { + return true; + } + throw error; + } +} + +function shutdownSimulator(udid: string): void { + try { + execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); + } catch { + // Already shutdown + } +} + +function waitForBoot(udid: string): boolean { + sleepSync(2); + for (let i = 0; i < 30; i++) { + if (isSimulatorBooted(udid)) { + return true; + } + sleepSync(1); + } + return false; +} + +function preloadMedia(udid: string): void { + // Add images and videos + const mediaFiles = [...MEDIA_FILES.images, ...MEDIA_FILES.videos]; + for (const filename of mediaFiles) { + const mediaPath = path.join(MEDIA_ROOT, filename); + if (!existsSync(mediaPath)) { + throw new Error(`Media file not found: ${filename}`); + } + execSync(`xcrun simctl addmedia ${udid} "${mediaPath}"`); + } + + // Copy PDFs to Files app Downloads folder + // copyFileToSimulator expects a DeviceWrapper with udid and log properties + // We create a minimal mock object since we're not in a test context + const mockDevice: Pick = { + udid, + log: () => {}, // Empty function (no need for logs during setup) + }; + + for (const filename of MEDIA_FILES.pdfs) { + const sourcePath = path.join(MEDIA_ROOT, filename); + if (!existsSync(sourcePath)) { + throw new Error(`PDF file not found: ${filename}`); + } + copyFileToSimulator(mockDevice as DeviceWrapper, filename); + } +} + +// Create N number of pre-loaded simulators by: +// Creating one "template" simulator, booting it, copying media over, shutting it down and then cloning it N-1 times +// (You can't copy to shutdown simulators and you can't clone booted simulators) +function createIOSSimulators(config: SimulatorConfig): Simulator[] { + console.log(`\nCreating ${config.totalSimulators} iOS simulators\n`); + + const startTime = Date.now(); + const simulators: Simulator[] = []; + + // Create Simulator 0 with preloaded media + console.log(`Creating Simulator 0 with preloaded media...`); + + const name0 = `Auto-${DEVICE_CONFIG.name}-0`; + const udid0 = createSimulator(name0, config.deviceType, config.runtime); + + if (!bootSimulator(udid0)) { + throw new Error('Failed to boot Simulator 0'); + } + + if (!waitForBoot(udid0)) { + throw new Error('Simulator 0 boot timeout'); + } + + preloadMedia(udid0); + shutdownSimulator(udid0); + + simulators.push({ + name: name0, + udid: udid0, + wdaPort: 1253, + index: 0, + }); + + console.log(`āœ“ ${name0} ready`); + + // Clone remaining simulators from Simulator 0 + console.log(`Cloning ${config.totalSimulators - 1} more Simulators...`); + + for (let index = 1; index < config.totalSimulators; index++) { + const name = `Auto-${DEVICE_CONFIG.name}-${index}`; + const udid = cloneSimulator(udid0, name); + + simulators.push({ + name, + udid, + wdaPort: 1253 + index, + index, + }); + + console.log(` [${index}/${config.totalSimulators - 1}] ${name}`); + } + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`\nāœ“ Created ${simulators.length} Simulators in ${totalTime}s`); + + saveSimulatorConfig(simulators); + return simulators; +} + +function updateLocalEnvFile(simulators: Simulator[]): void { + const envPath = '.env'; + let envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : ''; + + // Remove old simulator lines + envContent = envContent + .split('\n') + .filter(line => { + const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); + return !isSimLine; + }) + .join('\n'); + + // Add new simulator UDIDs + const simLines = simulators.map((sim, i) => `IOS_${i + 1}_SIMULATOR=${sim.udid}`).join('\n'); + + envContent = envContent.trim() + '\n\n# iOS Simulators (auto-generated)\n' + simLines + '\n'; + writeFileSync(envPath, envContent); +} + +function saveSimulatorConfig(simulators: Simulator[]): void { + // For running on CI, create a json file that GHA can read the UDIDs from + if (process.env.CI === '1') { + writeFileSync('ios-simulators.json', JSON.stringify(simulators, null, 2)); + console.log(`āœ“ Saved to ios-simulators.json`); + } else { + // For local development, update the IOS_N_SIMULATOR variables in .env + updateLocalEnvFile(simulators); + console.log(`āœ“ Updated .env`); + } +} + +// Main execution +const numSimulatorsArg = process.argv[2]; + +if (!numSimulatorsArg) { + console.error('Error: Number of Simulators required'); + console.error('Usage: yarn create-simulators '); + process.exit(1); +} + +const numSimulators = parseInt(numSimulatorsArg); + +if (isNaN(numSimulators) || numSimulators < 1) { + console.error('Error: Invalid number of Simulators'); + console.error('Usage: yarn create-simulators '); + process.exit(1); +} + +try { + createIOSSimulators({ + deviceType: DEVICE_CONFIG.type, + runtime: DEVICE_CONFIG.runtime, + totalSimulators: numSimulators, + }); +} catch (error) { + console.error('\nāœ— Failed to create Simulators'); + console.error(error); + process.exit(1); +} From 9b646c488ae9b84494f70bf5e3b595979e1264ba Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 10:09:24 +1000 Subject: [PATCH 03/15] fix: ensure ci runs use json --- .gitignore | 3 +- run/test/specs/utils/capabilities_ios.ts | 60 ++++++++++++------------ scripts/cleanup_ios_simulators.ts | 2 +- scripts/create_ios_simulators.ts | 6 +-- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 30b583c38..f4b3a32d2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,4 @@ avd/* .eslintcache test-results.csv *.csv -/allure* -ios-simulators.json \ No newline at end of file +/allure* \ No newline at end of file diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index b19e8cd4f..4ab6dc6fb 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -42,19 +42,9 @@ type Simulator = { }; function loadSimulators(): Simulator[] { - const jsonPath = 'ios-simulators.json'; - - // Try JSON first (CI with persistent simulators) - if (existsSync(jsonPath)) { - console.log('šŸ“± Looking for iOS simulators from ios-simulators.json'); - const content = readFileSync(jsonPath, 'utf-8'); - const sims: Simulator[] = JSON.parse(content); - console.log(` Found ${sims.length} simulators`); - return sims; - } + const jsonPath = 'ci-simulators.json'; - // Fallback to environment variables (local dev) - console.log('šŸ“± Looking for iOS simulators from environment variables'); + // Load from .env variables const envVars = [ 'IOS_1_SIMULATOR', 'IOS_2_SIMULATOR', @@ -74,31 +64,39 @@ function loadSimulators(): Simulator[] { .map((envVar, index) => { const udid = process.env[envVar]; if (!udid) return null; - - return { - name: `Sim-${index + 1}`, - udid, - wdaPort: 1253 + index, - index, - }; + return { name: `Sim-${index + 1}`, udid, wdaPort: 1253 + index, index }; }) .filter((sim): sim is Simulator => sim !== null); - // Re-index to be contiguous - return simulators.map((sim, newIndex) => ({ - ...sim, - wdaPort: 1253 + newIndex, - index: newIndex, - })); -} + // If we have simulators from env, use them (local dev) + if (simulators.length > 0) { + console.log(`šŸ“± Loaded ${simulators.length} simulators from .env`); + return simulators.map((sim, newIndex) => ({ + ...sim, + wdaPort: 1253 + newIndex, + index: newIndex, + })); + } -const simulators = loadSimulators(); + // No env simulators - check if we're on CI + if (process.env.CI === '1') { + // CI should use JSON + if (existsSync(jsonPath)) { + console.log('šŸ“± Loaded simulators from ios-simulators.json (CI)'); + const sims: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); + return sims; + } + throw new Error('CI mode: ios-simulators.json not found'); + } -if (simulators.length === 0) { - throw new Error('No iOS Simulators found.\n' + 'Run: yarn create-simulators '); + // Local dev with no .env entries + throw new Error( + 'No iOS simulators found in .env\n' + + 'Run: yarn create-simulators \n' + + 'Example: yarn create-simulators 4' + ); } - -console.log(`āœ“ Loaded ${simulators.length} iOS simulators`); +const simulators = loadSimulators(); const capabilities = simulators.map(sim => ({ ...sharediOSCapabilities, diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts index 81e405d9f..3d0675c9f 100644 --- a/scripts/cleanup_ios_simulators.ts +++ b/scripts/cleanup_ios_simulators.ts @@ -32,7 +32,7 @@ function deleteSimulator(udid: string): boolean { } function cleanupFromJSON(): number { - const jsonPath = 'ios-simulators.json'; + const jsonPath = 'ci-simulators.json'; if (!existsSync(jsonPath)) { return 0; diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index 39040093d..17241f11a 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -25,7 +25,7 @@ import { sleepSync } from './shared'; * * Usage: * yarn create-simulators 4 # Local: 4 Simulators - * yarn create-simulators 12 # CI: 12 Simulators + * CI=1 yarn create-simulators 12 # For CI: 12 Simulators */ type SimulatorConfig = { @@ -209,8 +209,8 @@ function updateLocalEnvFile(simulators: Simulator[]): void { function saveSimulatorConfig(simulators: Simulator[]): void { // For running on CI, create a json file that GHA can read the UDIDs from if (process.env.CI === '1') { - writeFileSync('ios-simulators.json', JSON.stringify(simulators, null, 2)); - console.log(`āœ“ Saved to ios-simulators.json`); + writeFileSync('ci-simulators.json', JSON.stringify(simulators, null, 2)); + console.log(`āœ“ Saved to ci-simulators.json`); } else { // For local development, update the IOS_N_SIMULATOR variables in .env updateLocalEnvFile(simulators); From d6665a1b7f34eb53148bb45b29df06e56a7e2823 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 10:13:29 +1000 Subject: [PATCH 04/15] feat: add ci simulators json file --- ci-simulators.json | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 ci-simulators.json diff --git a/ci-simulators.json b/ci-simulators.json new file mode 100644 index 000000000..eb2ba09ba --- /dev/null +++ b/ci-simulators.json @@ -0,0 +1,74 @@ +[ + { + "name": "Auto-16PM-0", + "udid": "4D0EDB6B-9517-4E9F-AD80-4853604401FB", + "wdaPort": 1253, + "index": 0 + }, + { + "name": "Auto-16PM-1", + "udid": "145BA489-0AAB-473F-9238-57C8AD75576A", + "wdaPort": 1254, + "index": 1 + }, + { + "name": "Auto-16PM-2", + "udid": "6F8F94E6-5623-4C8C-88A0-DAE34F343BCE", + "wdaPort": 1255, + "index": 2 + }, + { + "name": "Auto-16PM-3", + "udid": "5CFFE21B-26BE-4636-99FE-B5D7B8DC76C4", + "wdaPort": 1256, + "index": 3 + }, + { + "name": "Auto-16PM-4", + "udid": "570FEA9F-AFC2-4CCE-B637-290D0EE290C4", + "wdaPort": 1257, + "index": 4 + }, + { + "name": "Auto-16PM-5", + "udid": "09D47861-AF97-4D56-9DC1-9839168AA3CA", + "wdaPort": 1258, + "index": 5 + }, + { + "name": "Auto-16PM-6", + "udid": "3C7A031A-3224-40A9-86C7-BE64B8B6E0A2", + "wdaPort": 1259, + "index": 6 + }, + { + "name": "Auto-16PM-7", + "udid": "BA458AF8-C3F9-41E7-8B76-61157EA5EDF3", + "wdaPort": 1260, + "index": 7 + }, + { + "name": "Auto-16PM-8", + "udid": "5C799A8A-2AE0-4ED9-A077-BCC703ABF7E0", + "wdaPort": 1261, + "index": 8 + }, + { + "name": "Auto-16PM-9", + "udid": "AEE0AE84-26FA-42FE-85CD-82780DF1154C", + "wdaPort": 1262, + "index": 9 + }, + { + "name": "Auto-16PM-10", + "udid": "5B947D6C-DAE0-4066-9263-C2B3E1B4E970", + "wdaPort": 1263, + "index": 10 + }, + { + "name": "Auto-16PM-11", + "udid": "662F717D-A26D-47C7-A47B-E5090B1C4239", + "wdaPort": 1264, + "index": 11 + } +] \ No newline at end of file From ad508392e620a8663daf6330bf5bd944996023b4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 10:46:33 +1000 Subject: [PATCH 05/15] fix: don't clean up json in local dev --- .prettierignore | 3 ++- scripts/cleanup_ios_simulators.ts | 8 +++++++- scripts/create_ios_simulators.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.prettierignore b/.prettierignore index 405542119..a45fac514 100644 --- a/.prettierignore +++ b/.prettierignore @@ -31,4 +31,5 @@ test-results/ README.md package.json /avd/ -run/localizer/* \ No newline at end of file +run/localizer/* +ci-simulators.json \ No newline at end of file diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts index 3d0675c9f..291061ecd 100644 --- a/scripts/cleanup_ios_simulators.ts +++ b/scripts/cleanup_ios_simulators.ts @@ -34,6 +34,12 @@ function deleteSimulator(udid: string): boolean { function cleanupFromJSON(): number { const jsonPath = 'ci-simulators.json'; + // Only cleanup JSON on CI (it gets recreated there) + // On local dev, leave it alone (it's a tracked file for CI) + if (process.env.CI !== '1') { + return 0; + } + if (!existsSync(jsonPath)) { return 0; } @@ -50,7 +56,7 @@ function cleanupFromJSON(): number { } unlinkSync(jsonPath); - console.log(`āœ“ Removed ios-simulators.json`); + console.log(`āœ“ Removed ${jsonPath}`); return deleted; } diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index 17241f11a..fff8704b0 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -40,6 +40,7 @@ export type Simulator = { wdaPort: number; index: number; }; + // Define the device type and runtime to create const DEVICE_CONFIG = { type: 'iPhone 16 Pro Max', From 273b504283002c6f91a47c7c70180ae181b1bc19 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 11:17:36 +1000 Subject: [PATCH 06/15] chore: blacklist done/donate --- run/types/DeviceWrapper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5e2c5f0a6..b51d5115b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -331,6 +331,7 @@ export class DeviceWrapper { const blacklist = [ { from: 'Voice message', to: 'New voice message' }, { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, + { from: 'Done', to: 'Donate' } ]; // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores From 347f86309541d96623997e11c8451e469a0569af Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 11:21:35 +1000 Subject: [PATCH 07/15] chore: linting --- run/types/DeviceWrapper.ts | 4 ++-- scripts/cleanup_ios_simulators.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index b51d5115b..15dc7b26d 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -331,7 +331,7 @@ export class DeviceWrapper { const blacklist = [ { from: 'Voice message', to: 'New voice message' }, { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, - { from: 'Done', to: 'Donate' } + { from: 'Done', to: 'Donate' }, ]; // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores @@ -735,7 +735,7 @@ export class DeviceWrapper { } ); - return result; // or whatever you want to do with it + return result; } public async longPressConversation(userName: string) { diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts index 291061ecd..afb0e0875 100644 --- a/scripts/cleanup_ios_simulators.ts +++ b/scripts/cleanup_ios_simulators.ts @@ -39,7 +39,7 @@ function cleanupFromJSON(): number { if (process.env.CI !== '1') { return 0; } - + if (!existsSync(jsonPath)) { return 0; } From 7e5c05718a6463dd8147dff4b980a463ddfab839 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 11:32:27 +1000 Subject: [PATCH 08/15] refactor: move boot and shutdown to shared file --- scripts/cleanup_ios_simulators.ts | 9 +++------ scripts/create_ios_simulators.ts | 22 +--------------------- scripts/ios_shared.ts | 29 +++++++++++++++++++++++++++++ scripts/start_ios.ts | 17 ++--------------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts index afb0e0875..fc1660231 100644 --- a/scripts/cleanup_ios_simulators.ts +++ b/scripts/cleanup_ios_simulators.ts @@ -3,6 +3,8 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import type { Simulator } from './create_ios_simulators'; +import { shutdownSimulator } from './ios_shared'; + /** * iOS Simulator Cleanup Script * @@ -17,12 +19,7 @@ import type { Simulator } from './create_ios_simulators'; */ function deleteSimulator(udid: string): boolean { - try { - execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); - } catch { - // Already shutdown - } - + shutdownSimulator(udid); try { execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); return true; diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index fff8704b0..abe6250e5 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import type { DeviceWrapper } from '../run/types/DeviceWrapper'; import { copyFileToSimulator } from '../run/test/specs/utils/copy_file_to_simulator'; -import { isSimulatorBooted } from './ios_shared'; +import { bootSimulator, isSimulatorBooted, shutdownSimulator } from './ios_shared'; import { sleepSync } from './shared'; /** @@ -69,26 +69,6 @@ function cloneSimulator(sourceUdid: string, newName: string): string { return output; } -function bootSimulator(udid: string): boolean { - try { - execSync(`xcrun simctl boot ${udid}`, { stdio: 'pipe' }); - return true; - } catch (error: any) { - if (error.message?.includes('Unable to boot device in current state: Booted')) { - return true; - } - throw error; - } -} - -function shutdownSimulator(udid: string): void { - try { - execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); - } catch { - // Already shutdown - } -} - function waitForBoot(udid: string): boolean { sleepSync(2); for (let i = 0; i < 30; i++) { diff --git a/scripts/ios_shared.ts b/scripts/ios_shared.ts index fdad5e3c1..fec18e79e 100644 --- a/scripts/ios_shared.ts +++ b/scripts/ios_shared.ts @@ -6,6 +6,27 @@ export function getSimulatorUDID(index: number) { return process.env[envVar]; } +export function bootSimulator(udid: string, label?: number | string): boolean { + try { + if (label !== undefined) { + console.log(`Booting simulator ${label}: ${udid}`); + } + execSync(`xcrun simctl boot ${udid}`, { stdio: 'pipe' }); + return true; + } catch (error: any) { + if (error.message?.includes('Unable to boot device in current state: Booted')) { + if (label !== undefined) { + console.log(`Simulator ${label} already booted: ${udid}`); + } + return true; + } + + console.error(`Failed to boot simulator ${label || udid}`); + console.error(error.stderr?.toString() || error.message); + return false; + } +} + export function isSimulatorBooted(udid: string) { try { const result = execSync(`xcrun simctl list devices booted`).toString(); @@ -43,3 +64,11 @@ export function getAllSimulators() { export function getChunkedSimulators(chunkSize: number) { return chunk(getAllSimulators(), chunkSize); } + +export function shutdownSimulator(udid: string): void { + try { + execSync(`xcrun simctl shutdown ${udid}`, { stdio: 'pipe' }); + } catch { + // Already shutdown or doesn't exist - this is fine + } +} diff --git a/scripts/start_ios.ts b/scripts/start_ios.ts index edfbd8917..f43bf78b9 100644 --- a/scripts/start_ios.ts +++ b/scripts/start_ios.ts @@ -1,23 +1,10 @@ -import { execSync, spawnSync } from 'child_process'; +import { spawnSync } from 'child_process'; -import { getChunkedSimulators, isSimulatorBooted } from './ios_shared'; +import { bootSimulator, getChunkedSimulators, isSimulatorBooted } from './ios_shared'; import { sleepSync } from './shared'; const START_CHUNK = 12; -function bootSimulator(udid: string, label: number) { - try { - console.log(`Booting simulator ${label}: ${udid}`); - execSync(`xcrun simctl boot ${udid}`, { stdio: 'inherit' }); - } catch (error: any) { - console.error(`Error: Boot command failed for ${udid}`); - console.error(error.stderr?.toString() || error.message); - return false; - } - - return true; -} - function startSimulatorsFromEnvIOS() { console.log('Starting iOS simulators from environment variables...'); From 2407b4c5c5f1bc8c796ec429ef65c33ebe7a4bd3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 11:58:16 +1000 Subject: [PATCH 09/15] refactor dynamic simulator loading --- run/test/specs/utils/capabilities_ios.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 4ab6dc6fb..c9ed53f73 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -3,6 +3,8 @@ import { W3CCapabilities } from '@wdio/types/build/Capabilities'; import dotenv from 'dotenv'; import { existsSync, readFileSync } from 'fs'; +import { IntRange } from '../../../types/RangeType'; + dotenv.config(); const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; @@ -104,13 +106,23 @@ const capabilities = simulators.map(sim => ({ 'appium:wdaLocalPort': sim.wdaPort, })); -export const MAX_CAPABILITIES_INDEX = capabilities.length; -export type CapabilitiesIndexType = number; +// Use a constant max that matches the env vars array length for type safety +const _MAX_CAPABILITIES_INDEX = 12 as const; + +// For runtime validation, we need to check against actual loaded simulators +export const getMaxCapabilitiesIndex = () => capabilities.length; + +// Type is still based on the constant for compile-time safety +export type CapabilitiesIndexType = IntRange<0, typeof _MAX_CAPABILITIES_INDEX>; export function capabilityIsValid( capabilitiesIndex: number ): capabilitiesIndex is CapabilitiesIndexType { - return capabilitiesIndex >= 0 && capabilitiesIndex < MAX_CAPABILITIES_INDEX; + // Runtime validation against actual loaded capabilities + if (capabilitiesIndex < 0 || capabilitiesIndex >= capabilities.length) { + return false; + } + return true; } export function getIosCapabilities(capabilitiesIndex: CapabilitiesIndexType): W3CCapabilities { From 6c67180336b0ba07f79be240e5aae57b1abe8cba Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 15:58:58 +1000 Subject: [PATCH 10/15] feat: cycle through worker pool on retry linting --- run/test/specs/utils/open_app.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index ac427e820..a1208fcc6 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -292,9 +292,21 @@ const openiOSApp = async ( }> => { console.info('openiOSApp'); - // Calculate the actual capabilities index for the current worker - const actualCapabilitiesIndex = - capabilitiesIndex + getDevicesPerTestCount() * parseInt(process.env.TEST_PARALLEL_INDEX || '0'); + const parallelIndex = parseInt(process.env.TEST_PARALLEL_INDEX || '0'); + const devicesPerWorker = getDevicesPerTestCount(); + + // Calculate base offset for this worker + const workerBaseOffset = devicesPerWorker * parallelIndex; + + // Add retry offset, but wrap within the worker's device pool only + // This means when retrying, a alice/bob etc won't be the same device as before within a worker's pool + const retryOffset = testInfo.retry || 0; + const deviceIndexWithinWorker = (capabilitiesIndex + retryOffset) % devicesPerWorker; + const actualCapabilitiesIndex = workerBaseOffset + deviceIndexWithinWorker; + + console.info( + `Worker ${parallelIndex}, Base Device ${capabilitiesIndex}, Retry ${retryOffset} -> Device ${actualCapabilitiesIndex}` + ); const opts: XCUITestDriverOpts = { address: `http://localhost:${APPIUM_PORT}`, From 09d4c6a6b60d42732da2818c97635f988c6ea15c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 2 Oct 2025 16:22:05 +1000 Subject: [PATCH 11/15] chore: make note of DEVICES_PER_TEST_COUNT --- run/test/specs/utils/open_app.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index a1208fcc6..c313bdcb4 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -293,20 +293,24 @@ const openiOSApp = async ( console.info('openiOSApp'); const parallelIndex = parseInt(process.env.TEST_PARALLEL_INDEX || '0'); - const devicesPerWorker = getDevicesPerTestCount(); - // Calculate base offset for this worker + // NOTE: This assumes DEVICES_PER_TEST_COUNT=4 is set in CI for iOS (not applicable to Android) + // Worker pools are fixed at 4 devices each regardless of actual test size: + // Worker 0: devices 0-3, Worker 1: devices 4-7, Worker 2: devices 8-11 + const devicesPerWorker = getDevicesPerTestCount(); const workerBaseOffset = devicesPerWorker * parallelIndex; // Add retry offset, but wrap within the worker's device pool only - // This means when retrying, a alice/bob etc won't be the same device as before within a worker's pool + // This means when retrying, alice/bob etc won't be the same device as before within a worker's pool const retryOffset = testInfo.retry || 0; const deviceIndexWithinWorker = (capabilitiesIndex + retryOffset) % devicesPerWorker; const actualCapabilitiesIndex = workerBaseOffset + deviceIndexWithinWorker; - console.info( - `Worker ${parallelIndex}, Base Device ${capabilitiesIndex}, Retry ${retryOffset} -> Device ${actualCapabilitiesIndex}` - ); + if (retryOffset > 0) { + console.info( + `Retry offset applied (#${retryOffset}), rotating device allocations within worker` + ); + } const opts: XCUITestDriverOpts = { address: `http://localhost:${APPIUM_PORT}`, From d28bda1f568622f991be2996bc3892b708e302d5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 16:54:38 +1100 Subject: [PATCH 12/15] chore: tidy up comments --- run/test/specs/utils/capabilities_ios.ts | 10 +++++----- run/test/specs/utils/open_app.ts | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index c9ed53f73..d68708882 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -65,14 +65,14 @@ function loadSimulators(): Simulator[] { const simulators = envVars .map((envVar, index) => { const udid = process.env[envVar]; - if (!udid) return null; + if (!udid) return null; // No need for all 12 sim variables to be set return { name: `Sim-${index + 1}`, udid, wdaPort: 1253 + index, index }; }) .filter((sim): sim is Simulator => sim !== null); // If we have simulators from env, use them (local dev) if (simulators.length > 0) { - console.log(`šŸ“± Loaded ${simulators.length} simulators from .env`); + console.log(`Using ${simulators.length} simulators from .env file`); return simulators.map((sim, newIndex) => ({ ...sim, wdaPort: 1253 + newIndex, @@ -84,7 +84,7 @@ function loadSimulators(): Simulator[] { if (process.env.CI === '1') { // CI should use JSON if (existsSync(jsonPath)) { - console.log('šŸ“± Loaded simulators from ios-simulators.json (CI)'); + console.log('Using simulators from ios-simulators.json (CI)'); const sims: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); return sims; } @@ -106,10 +106,10 @@ const capabilities = simulators.map(sim => ({ 'appium:wdaLocalPort': sim.wdaPort, })); -// Use a constant max that matches the env vars array length for type safety +// Use a constant max that matches the envVars array length for type safety const _MAX_CAPABILITIES_INDEX = 12 as const; -// For runtime validation, we need to check against actual loaded simulators +// For runtime validation, check against actual loaded simulators export const getMaxCapabilitiesIndex = () => capabilities.length; // Type is still based on the constant for compile-time safety diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index c313bdcb4..6c7147235 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -300,8 +300,10 @@ const openiOSApp = async ( const devicesPerWorker = getDevicesPerTestCount(); const workerBaseOffset = devicesPerWorker * parallelIndex; - // Add retry offset, but wrap within the worker's device pool only + // Apply retry offset, but wrap within the worker's device pool only // This means when retrying, alice/bob etc won't be the same device as before within a worker's pool + // This is to avoid any issues where a device might be in a bad state for some reason + // (e.g. not accessing photo library on iOS) const retryOffset = testInfo.retry || 0; const deviceIndexWithinWorker = (capabilitiesIndex + retryOffset) % devicesPerWorker; const actualCapabilitiesIndex = workerBaseOffset + deviceIndexWithinWorker; From 5e870ab6a872ea71dce2aecf2ce6dcfb180bec16 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 16:54:54 +1100 Subject: [PATCH 13/15] chore: linting --- run/test/specs/utils/open_app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index 6c7147235..f523109ca 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -302,7 +302,7 @@ const openiOSApp = async ( // Apply retry offset, but wrap within the worker's device pool only // This means when retrying, alice/bob etc won't be the same device as before within a worker's pool - // This is to avoid any issues where a device might be in a bad state for some reason + // This is to avoid any issues where a device might be in a bad state for some reason // (e.g. not accessing photo library on iOS) const retryOffset = testInfo.retry || 0; const deviceIndexWithinWorker = (capabilitiesIndex + retryOffset) % devicesPerWorker; From 1aada4ee3054ee9426d7d0ed3e9b70e128f89824 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 17:03:27 +1100 Subject: [PATCH 14/15] fix: remove env vars from android file too --- .github/workflows/android-regression.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index b19dbcb3e..ac5d80671 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -71,18 +71,6 @@ jobs: _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) - IOS_1_SIMULATOR: '' - IOS_2_SIMULATOR: '' - IOS_3_SIMULATOR: '' - IOS_4_SIMULATOR: '' - IOS_5_SIMULATOR: '' - IOS_6_SIMULATOR: '' - IOS_7_SIMULATOR: '' - IOS_8_SIMULATOR: '' - IOS_9_SIMULATOR: '' - IOS_10_SIMULATOR: '' - IOS_11_SIMULATOR: '' - IOS_12_SIMULATOR: '' steps: - uses: actions/checkout@v4 From c9a4ad25725065064836b43ef5bb7c1e3a0ed904 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 7 Oct 2025 15:59:19 +1100 Subject: [PATCH 15/15] fix: address PR comments eliminate unused variables, redundant deletion loops and types --- ci-simulators.json | 36 ++++++---------- run/test/specs/utils/capabilities_ios.ts | 11 ++--- scripts/cleanup_ios_simulators.ts | 54 +++++++++--------------- scripts/create_ios_simulators.ts | 16 ++----- 4 files changed, 39 insertions(+), 78 deletions(-) diff --git a/ci-simulators.json b/ci-simulators.json index eb2ba09ba..1b9e9aff8 100644 --- a/ci-simulators.json +++ b/ci-simulators.json @@ -2,73 +2,61 @@ { "name": "Auto-16PM-0", "udid": "4D0EDB6B-9517-4E9F-AD80-4853604401FB", - "wdaPort": 1253, - "index": 0 + "wdaPort": 1253 }, { "name": "Auto-16PM-1", "udid": "145BA489-0AAB-473F-9238-57C8AD75576A", - "wdaPort": 1254, - "index": 1 + "wdaPort": 1254 }, { "name": "Auto-16PM-2", "udid": "6F8F94E6-5623-4C8C-88A0-DAE34F343BCE", - "wdaPort": 1255, - "index": 2 + "wdaPort": 1255 }, { "name": "Auto-16PM-3", "udid": "5CFFE21B-26BE-4636-99FE-B5D7B8DC76C4", - "wdaPort": 1256, - "index": 3 + "wdaPort": 1256 }, { "name": "Auto-16PM-4", "udid": "570FEA9F-AFC2-4CCE-B637-290D0EE290C4", - "wdaPort": 1257, - "index": 4 + "wdaPort": 1257 }, { "name": "Auto-16PM-5", "udid": "09D47861-AF97-4D56-9DC1-9839168AA3CA", - "wdaPort": 1258, - "index": 5 + "wdaPort": 1258 }, { "name": "Auto-16PM-6", "udid": "3C7A031A-3224-40A9-86C7-BE64B8B6E0A2", - "wdaPort": 1259, - "index": 6 + "wdaPort": 1259 }, { "name": "Auto-16PM-7", "udid": "BA458AF8-C3F9-41E7-8B76-61157EA5EDF3", - "wdaPort": 1260, - "index": 7 + "wdaPort": 1260 }, { "name": "Auto-16PM-8", "udid": "5C799A8A-2AE0-4ED9-A077-BCC703ABF7E0", - "wdaPort": 1261, - "index": 8 + "wdaPort": 1261 }, { "name": "Auto-16PM-9", "udid": "AEE0AE84-26FA-42FE-85CD-82780DF1154C", - "wdaPort": 1262, - "index": 9 + "wdaPort": 1262 }, { "name": "Auto-16PM-10", "udid": "5B947D6C-DAE0-4066-9263-C2B3E1B4E970", - "wdaPort": 1263, - "index": 10 + "wdaPort": 1263 }, { "name": "Auto-16PM-11", "udid": "662F717D-A26D-47C7-A47B-E5090B1C4239", - "wdaPort": 1264, - "index": 11 + "wdaPort": 1264 } ] \ No newline at end of file diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index d68708882..713457a2a 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -36,11 +36,10 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { }, } as AppiumXCUITestCapabilities; -type Simulator = { +export type Simulator = { name: string; udid: string; wdaPort: number; - index: number; }; function loadSimulators(): Simulator[] { @@ -66,18 +65,14 @@ function loadSimulators(): Simulator[] { .map((envVar, index) => { const udid = process.env[envVar]; if (!udid) return null; // No need for all 12 sim variables to be set - return { name: `Sim-${index + 1}`, udid, wdaPort: 1253 + index, index }; + return { name: `Sim-${index + 1}`, udid, wdaPort: 1253 + index }; }) .filter((sim): sim is Simulator => sim !== null); // If we have simulators from env, use them (local dev) if (simulators.length > 0) { console.log(`Using ${simulators.length} simulators from .env file`); - return simulators.map((sim, newIndex) => ({ - ...sim, - wdaPort: 1253 + newIndex, - index: newIndex, - })); + return simulators; } // No env simulators - check if we're on CI diff --git a/scripts/cleanup_ios_simulators.ts b/scripts/cleanup_ios_simulators.ts index fc1660231..0e6cb2501 100644 --- a/scripts/cleanup_ios_simulators.ts +++ b/scripts/cleanup_ios_simulators.ts @@ -1,7 +1,7 @@ import { execSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; -import type { Simulator } from './create_ios_simulators'; +import type { Simulator } from '../run/test/specs/utils/capabilities_ios'; import { shutdownSimulator } from './ios_shared'; @@ -17,17 +17,22 @@ import { shutdownSimulator } from './ios_shared'; * Usage: * yarn cleanup-simulators */ +function deleteSimulator(udids: string[] | string): number { + const udidArray = Array.isArray(udids) ? udids : [udids]; + let deleted = 0; -function deleteSimulator(udid: string): boolean { - shutdownSimulator(udid); - try { - execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); - return true; - } catch { - return false; + for (const udid of udidArray) { + shutdownSimulator(udid); + try { + execSync(`xcrun simctl delete ${udid}`, { stdio: 'pipe' }); + deleted++; + } catch { + console.warn(`Failed to delete: ${udid}`); + } } -} + return deleted; +} function cleanupFromJSON(): number { const jsonPath = 'ci-simulators.json'; @@ -42,15 +47,7 @@ function cleanupFromJSON(): number { } const simulators: Simulator[] = JSON.parse(readFileSync(jsonPath, 'utf-8')); - let deleted = 0; - - for (const sim of simulators) { - if (deleteSimulator(sim.udid)) { - deleted++; - } else { - console.warn(`Failed to delete: ${sim.udid}`); - } - } + const deleted = deleteSimulator(simulators.map(sim => sim.udid)); unlinkSync(jsonPath); console.log(`āœ“ Removed ${jsonPath}`); @@ -68,10 +65,12 @@ function cleanupFromEnv(): number { const envContent = readFileSync(envPath, 'utf-8'); const lines = envContent.split('\n'); + const simulatorPattern = /^IOS_\d+_SIMULATOR=/; + const simulatorExtractPattern = /^IOS_\d+_SIMULATOR=(.+)/; + const udids = lines - .filter(line => line.trim().startsWith('IOS_') && line.includes('_SIMULATOR=')) .map(line => { - const match = line.match(/IOS_\d+_SIMULATOR=(.+)/); + const match = line.match(simulatorExtractPattern); return match ? match[1].trim() : null; }) .filter((udid): udid is string => udid !== null); @@ -80,23 +79,12 @@ function cleanupFromEnv(): number { return 0; } - let deleted = 0; - for (const udid of udids) { - if (deleteSimulator(udid)) { - deleted++; - } else { - console.warn(`Failed to delete: ${udid}`); - } - } + const deleted = deleteSimulator(udids); // Remove simulator lines from .env const cleanedEnv = lines - .filter(line => { - const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); - const isSimComment = line.trim().startsWith('# iOS Simulators'); - return !isSimLine && !isSimComment; - }) + .filter(line => !simulatorPattern.test(line.trim())) .join('\n') .trim() + '\n'; diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index abe6250e5..81e63f3c8 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -2,6 +2,7 @@ import { execSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import * as path from 'path'; +import type { Simulator } from '../run/test/specs/utils/capabilities_ios'; import type { DeviceWrapper } from '../run/types/DeviceWrapper'; import { copyFileToSimulator } from '../run/test/specs/utils/copy_file_to_simulator'; @@ -34,13 +35,6 @@ type SimulatorConfig = { totalSimulators: number; }; -export type Simulator = { - name: string; - udid: string; - wdaPort: number; - index: number; -}; - // Define the device type and runtime to create const DEVICE_CONFIG = { type: 'iPhone 16 Pro Max', @@ -138,7 +132,6 @@ function createIOSSimulators(config: SimulatorConfig): Simulator[] { name: name0, udid: udid0, wdaPort: 1253, - index: 0, }); console.log(`āœ“ ${name0} ready`); @@ -154,7 +147,6 @@ function createIOSSimulators(config: SimulatorConfig): Simulator[] { name, udid, wdaPort: 1253 + index, - index, }); console.log(` [${index}/${config.totalSimulators - 1}] ${name}`); @@ -172,12 +164,10 @@ function updateLocalEnvFile(simulators: Simulator[]): void { let envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : ''; // Remove old simulator lines + const simulatorPattern = /^IOS_\d+_SIMULATOR=/; envContent = envContent .split('\n') - .filter(line => { - const isSimLine = line.trim().startsWith('IOS_') && line.includes('_SIMULATOR='); - return !isSimLine; - }) + .filter(line => !simulatorPattern.test(line.trim())) .join('\n'); // Add new simulator UDIDs