chore(e2e): Change iOS simulator to 26.1 from 17.2 (#9370)

This commit is contained in:
yasser khan 2026-01-30 08:40:22 +05:30 committed by GitHub
parent d014856a3a
commit fdc559d70d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 745 additions and 99 deletions

View file

@ -48,7 +48,7 @@ jobs:
build-ios-simulator: build-ios-simulator:
if: contains(github.event.label.name, 'E2E iOS tests for PR') if: contains(github.event.label.name, 'E2E iOS tests for PR')
runs-on: macos-15 runs-on: macos-26
needs: needs:
- update-initial-status-ios - update-initial-status-ios
steps: steps:
@ -61,6 +61,10 @@ jobs:
intune-enabled: ${{ env.INTUNE_ENABLED }} intune-enabled: ${{ env.INTUNE_ENABLED }}
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }} intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
- name: Set .env with RUNNING_E2E=true
run: |
echo "RUNNING_E2E=true" > .env
- name: Build iOS Simulator - name: Build iOS Simulator
env: env:
TAG: "${{ github.event.pull_request.head.sha }}" TAG: "${{ github.event.pull_request.head.sha }}"
@ -74,7 +78,7 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: ios-build-simulator-${{ github.run_id }} name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip path: Mattermost-simulator-*.app.zip
build-android-apk: build-android-apk:
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores

View file

@ -38,12 +38,12 @@ on:
description: "iPhone simulator name" description: "iPhone simulator name"
required: false required: false
type: string type: string
default: "iPhone 15 Pro" default: "iPhone 17 Pro"
ios_device_os_name: ios_device_os_name:
description: "iPhone simulator OS version" description: "iPhone simulator OS version"
required: false required: false
type: string type: string
default: "iOS 17.2" default: "iOS 26.2"
low_bandwidth_mode: low_bandwidth_mode:
description: "Enable low bandwidth mode" description: "Enable low bandwidth mode"
required: false required: false
@ -115,7 +115,7 @@ jobs:
id: generate-specs id: generate-specs
uses: ./.github/actions/generate-specs uses: ./.github/actions/generate-specs
with: with:
parallelism: 10 parallelism: 5
search_path: detox/e2e/test search_path: detox/e2e/test
device_name: ${{ env.DEVICE_NAME }} device_name: ${{ env.DEVICE_NAME }}
device_os_version: ${{ env.DEVICE_OS_VERSION }} device_os_version: ${{ env.DEVICE_OS_VERSION }}
@ -173,6 +173,82 @@ jobs:
- name: Install Detox Dependencies - name: Install Detox Dependencies
run: cd detox && npm i run: cd detox && npm i
- name: Pre-boot iOS Simulator
run: |
set -e
echo "Looking for simulator: ${{ env.DEVICE_NAME }} with ${{ env.DEVICE_OS_VERSION }}"
# Find or create the simulator
SIMULATOR_ID=$(xcrun simctl list devices | grep "${{ env.DEVICE_NAME }}" | grep "${{ env.DEVICE_OS_VERSION }}" | head -1 | grep -oE '([0-9A-F-]{36})' || true)
if [ -z "$SIMULATOR_ID" ]; then
echo "Simulator not found. Creating simulator ${{ env.DEVICE_NAME }} with ${{ env.DEVICE_OS_VERSION }}"
# Get device type (use exact match with parentheses to avoid partial matches)
DEVICE_TYPE=$(xcrun simctl list devicetypes | grep "${{ env.DEVICE_NAME }} (" | head -1 | awk -F'[()]' '{print $(NF-1)}')
if [ -z "$DEVICE_TYPE" ]; then
echo "Error: Could not find device type for ${{ env.DEVICE_NAME }}"
exit 1
fi
echo "Found device type: $DEVICE_TYPE"
# Get runtime (extract the identifier after the last " - ")
RUNTIME=$(xcrun simctl list runtimes | grep "${{ env.DEVICE_OS_VERSION }}" | head -1 | sed 's/.* - \(.*\)/\1/')
if [ -z "$RUNTIME" ]; then
echo "Error: Could not find runtime for ${{ env.DEVICE_OS_VERSION }}"
exit 1
fi
echo "Found runtime: $RUNTIME"
# Create simulator
SIMULATOR_ID=$(xcrun simctl create "CI-${{ env.DEVICE_NAME }}" "$DEVICE_TYPE" "$RUNTIME")
echo "Created simulator with ID: $SIMULATOR_ID"
else
echo "Found existing simulator: $SIMULATOR_ID"
fi
# Boot simulator first to initialize directory structure
echo "Initializing simulator directories (first boot)..."
xcrun simctl boot "$SIMULATOR_ID"
xcrun simctl bootstatus "$SIMULATOR_ID"
# Shut down to apply password autofill restrictions
echo "Shutting down to apply password autofill restrictions..."
xcrun simctl shutdown "$SIMULATOR_ID"
sleep 3
echo "Disabling password autofill (simulator is shut down)"
# Ensure the directory exists (should now exist after first boot)
SETTINGS_DIR="$HOME/Library/Developer/CoreSimulator/Devices/$SIMULATOR_ID/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles"
mkdir -p "$SETTINGS_DIR"
# Use the JavaScript utility to disable password autofill
cd detox
node utils/disable_ios_autofill.js --simulator-id "$SIMULATOR_ID"
if [ $? -ne 0 ]; then
echo "⚠️ Failed to disable password autofill"
exit 1
fi
cd ..
echo "Booting simulator with password autofill disabled..."
xcrun simctl boot "$SIMULATOR_ID"
# Wait for boot to complete
xcrun simctl bootstatus "$SIMULATOR_ID"
# Open Simulator.app in background to speed up UI rendering
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID"
# Give Simulator.app time to fully initialize UI
sleep 3
echo "SIMULATOR_ID=$SIMULATOR_ID" >> $GITHUB_ENV
- name: Start Proxy - name: Start Proxy
if: ${{ inputs.low_bandwidth_mode }} if: ${{ inputs.low_bandwidth_mode }}
id: start-proxy id: start-proxy
@ -182,17 +258,73 @@ jobs:
- name: Set .env with RUNNING_E2E=true - name: Set .env with RUNNING_E2E=true
run: | run: |
cat > .env <<EOF echo "RUNNING_E2E=true" > .env
echo "RUNNING_E2E=true" >> .env
- name: Run Detox E2E Tests - name: Verify Simulator Health
run: |
echo "Verifying simulator health before running tests..."
# Check if simulator is still responsive
if ! xcrun simctl bootstatus "$SIMULATOR_ID" -b 2>/dev/null; then
echo "⚠️ Simulator not responsive, attempting recovery..."
# Try to shutdown and reboot
xcrun simctl shutdown "$SIMULATOR_ID" || true
sleep 3
xcrun simctl boot "$SIMULATOR_ID"
xcrun simctl bootstatus "$SIMULATOR_ID"
fi
echo "✅ Simulator is healthy and ready"
- name: Start Metro Bundler and Run Detox E2E Tests
continue-on-error: true # We want to run all the tests continue-on-error: true # We want to run all the tests
run: | run: |
# Start the server # Start Metro bundler in background
npm run start & npm run start > metro.log 2>&1 &
sleep 120 # Wait for watchman to finish querying the files METRO_PID=$!
echo "Metro PID: $METRO_PID"
echo "METRO_PID=$METRO_PID" >> $GITHUB_ENV
# Wait for Metro to be ready with health check endpoint polling
timeout=120
elapsed=0
echo "Waiting for Metro bundler to be ready..."
while [ $elapsed -lt $timeout ]; do
# Try health check endpoint first (more reliable)
if curl -s http://localhost:8081/status 2>/dev/null | grep -q "packager-status:running"; then
echo "✅ Metro bundler is ready after ${elapsed}s (verified via health check)"
break
# Fallback to log checking
elif grep -q "Fast - Scalable - Integrated" metro.log 2>/dev/null; then
echo "✅ Metro bundler is ready after ${elapsed}s (verified via logs)"
break
fi
# Show progress every 10 seconds
if [ $((elapsed % 10)) -eq 0 ] && [ $elapsed -gt 0 ]; then
echo "Still waiting for Metro... (${elapsed}s elapsed)"
fi
sleep 2
elapsed=$((elapsed + 2))
done
if [ $elapsed -ge $timeout ]; then
echo "⚠️ Warning: Metro bundler may not be fully ready after ${timeout}s, proceeding anyway"
echo "Metro process status:"
ps aux | grep -E "metro|node.*start" | grep -v grep || echo "Metro process not found"
echo ""
echo "Last 30 lines of metro.log:"
tail -30 metro.log
else
echo "Metro is serving on http://localhost:8081"
# Give Metro extra time to fully initialize for iOS 26.2
echo "Allowing Metro to fully stabilize..."
sleep 3
fi
cd detox cd detox
npm run clean-detox
npm run detox:config-gen npm run detox:config-gen
npm run e2e:ios-test -- ${{ matrix.specs }} npm run e2e:ios-test -- ${{ matrix.specs }}
env: env:
@ -203,6 +335,26 @@ jobs:
DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }} DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }}
LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }} LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }}
- name: Cleanup Simulator State
if: always()
run: |
echo "Cleaning up simulator state after tests..."
# Stop Metro bundler if still running
if [ ! -z "$METRO_PID" ]; then
echo "Stopping Metro bundler (PID: $METRO_PID)..."
kill -9 "$METRO_PID" 2>/dev/null || true
fi
# Terminate app gracefully
if [ ! -z "$SIMULATOR_ID" ]; then
echo "Terminating app on simulator..."
xcrun simctl terminate "$SIMULATOR_ID" com.mattermost.rnbeta 2>/dev/null || true
sleep 2
fi
echo "✅ Simulator cleanup complete"
- name: reset network settings - name: reset network settings
if: ${{ inputs.low_bandwidth_mode || failure() }} if: ${{ inputs.low_bandwidth_mode || failure() }}
run: | run: |

View file

@ -82,16 +82,16 @@
}, },
"behavior": { "behavior": {
"init": { "init": {
"reinstallApp": true, "reinstallApp": false,
"launchApp": false "launchApp": true
}, },
"cleanup": { "cleanup": {
"shutdownDevice": false "shutdownDevice": true
} }
}, },
"session": { "session": {
"sessionId": "mobile-test-session", "sessionId": "mobile-test-session",
"debugSynchronization": 5000, "debugSynchronization": 20000,
"autoStart": true "autoStart": true
}, },
"visibility": { "visibility": {

View file

@ -79,6 +79,30 @@ npm run e2e:ios-test
npm run e2e:android-test path to test file. npm run e2e:android-test path to test file.
``` ```
### Disabling Password Autofill (Optional)
iOS password autofill can interfere with login tests by automatically filling credentials. To disable this feature on your simulator:
```sh
# Interactive mode - select simulator from list
npm run e2e:ios-disable-autofill
# Or specify simulator ID directly
npm run e2e:ios-disable-autofill -- --simulator-id SIMULATOR_UDID
```
**When to use this:**
- Before running iOS E2E tests if you notice password fields being auto-filled
- When login tests fail unexpectedly due to autofill interference
- After creating a new iOS simulator for testing
**Note:** CI environments automatically disable this setting, so this is only needed for local development.
**Finding your simulator ID:**
```sh
xcrun simctl list devices | grep Booted
```
#### TIP : For iOS, you can download the simulator from `~Mobile: Test build` or `~Release: Mobile Apps` channel in the community. #### TIP : For iOS, you can download the simulator from `~Mobile: Test build` or `~Release: Mobile Apps` channel in the community.
### Results ### Results

View file

@ -8,7 +8,7 @@ module.exports = {
setupFilesAfterEnv: ['./test/setup.ts'], setupFilesAfterEnv: ['./test/setup.ts'],
maxWorkers: 1, maxWorkers: 1,
testSequencer: './custom_sequencer.js', testSequencer: './custom_sequencer.js',
testTimeout: 180000, testTimeout: process.env.LOW_BANDWIDTH_MODE === 'true' ? 240000 : 180000,
rootDir: '.', rootDir: '.',
testMatch: ['<rootDir>/test/**/*.e2e.ts'], testMatch: ['<rootDir>/test/**/*.e2e.ts'],
transform: { transform: {

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Alert} from '@support/ui/component'; import {Alert} from '@support/ui/component';
import {isAndroid, isIos, timeouts, wait, waitForElementToBeVisible} from '@support/utils'; import {isAndroid, isIos, timeouts, wait, waitForElementToBeVisible, waitForVisibilityWithRetry} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
class ServerScreen { class ServerScreen {
@ -47,7 +47,7 @@ class ServerScreen {
toBeVisible = async () => { toBeVisible = async () => {
await waitFor(this.serverScreen).toExist().withTimeout(timeouts.TEN_SEC); await waitFor(this.serverScreen).toExist().withTimeout(timeouts.TEN_SEC);
await waitFor(this.serverUrlInput).toBeVisible().withTimeout(timeouts.TEN_SEC); await waitForVisibilityWithRetry(this.serverUrlInput, timeouts.TEN_SEC);
return this.serverScreen; return this.serverScreen;
}; };

View file

@ -23,3 +23,86 @@ export async function scrollUntilVisible(testID: string, text: string): Promise<
export async function waitForLoadingSpinner(testID: string, timeout = 10000): Promise<void> { export async function waitForLoadingSpinner(testID: string, timeout = 10000): Promise<void> {
await waitFor(element(by.id(testID))).not.toBeVisible().withTimeout(timeout); await waitFor(element(by.id(testID))).not.toBeVisible().withTimeout(timeout);
} }
/**
* Safely dismiss keyboard with retry logic
* Prevents keyboard input session invalidation errors
* @param maxAttempts - Maximum number of dismissal attempts
*/
export async function dismissKeyboardSafely(maxAttempts = 3): Promise<void> {
/* eslint-disable no-await-in-loop, no-console */
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await device.pressBack(); // Works on both iOS (dismisses keyboard) and Android
// Wait a bit for keyboard to dismiss
await new Promise((resolve) => setTimeout(resolve, 300));
return;
} catch (error) {
if (attempt === maxAttempts) {
// On final attempt, log warning but don't throw to prevent test failures
console.warn(`[dismissKeyboardSafely] Failed to dismiss keyboard after ${maxAttempts} attempts:`, error);
} else {
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
}
/* eslint-enable no-await-in-loop, no-console */
}
/**
* Perform text input with keyboard state verification
* Ensures text input session is valid before typing
* @param element - Detox element to type into
* @param text - Text to type
*/
export async function typeTextSafely(elementToType: Detox.IndexableNativeElement, text: string): Promise<void> {
try {
// Tap element to ensure it's focused
await elementToType.tap();
await new Promise((resolve) => setTimeout(resolve, 300));
// Type text
await elementToType.typeText(text);
} catch (error) {
// eslint-disable-next-line no-console
console.warn('[typeTextSafely] Text input failed, retrying:', error);
// Retry once with dismissal and refocus
await dismissKeyboardSafely();
await new Promise((resolve) => setTimeout(resolve, 500));
await elementToType.tap();
await new Promise((resolve) => setTimeout(resolve, 300));
await elementToType.typeText(text);
}
}
/**
* Retry an element visibility check with exponential backoff
* Helps handle race conditions during navigation and UI transitions
* @param elementToCheck - Detox element to check visibility
* @param timeout - Timeout for each attempt in milliseconds
* @param maxAttempts - Maximum number of retry attempts
*/
export async function waitForVisibilityWithRetry(
elementToCheck: Detox.NativeElement,
timeout = 10000,
maxAttempts = 3,
): Promise<void> {
/* eslint-disable no-await-in-loop, no-console */
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await waitFor(elementToCheck).toBeVisible().withTimeout(timeout);
return;
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.warn(`[waitForVisibilityWithRetry] Attempt ${attempt}/${maxAttempts} failed, retrying...`);
// Exponential backoff: 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
/* eslint-enable no-await-in-loop, no-console */
}

View file

@ -76,7 +76,6 @@ describe('Channels - Unarchive Channel', () => {
await expect(ChannelScreen.introDisplayName).toHaveText(channelDisplayName); await expect(ChannelScreen.introDisplayName).toHaveText(channelDisplayName);
// # Go back to channel list screen by closing archived channel // # Go back to channel list screen by closing archived channel
await expect(ChannelScreen.archievedCloseChannelButton).toBeVisible();
// # Open channel info screen, tap on unarchive channel and confirm, close and re-open app to reload, and re-open unarchived public channel // # Open channel info screen, tap on unarchive channel and confirm, close and re-open app to reload, and re-open unarchived public channel
await ChannelInfoScreen.open(); await ChannelInfoScreen.open();

View file

@ -15,6 +15,59 @@ const RETRY_DELAY = 5000;
let isFirstLaunch = true; let isFirstLaunch = true;
/**
* Verify Detox connection to app is healthy
* @param maxAttempts - Maximum number of verification attempts
* @param delayMs - Delay between attempts in milliseconds
* @returns {Promise<void>}
*/
async function verifyDetoxConnection(maxAttempts = 3, delayMs = 2000): Promise<void> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
// Simple health check: verify device is responsive
device.getPlatform();
console.info(`✅ Detox connection verified on attempt ${attempt}`);
return;
} catch (error) {
console.warn(`❌ Detox connection check failed on attempt ${attempt}/${maxAttempts}: ${(error as Error).message}`);
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt)); // Exponential backoff
}
}
}
throw new Error('Detox connection verification failed after maximum attempts');
}
/**
* Wait for app to be ready (database initialized, bridge ready)
* @param timeoutMs - Maximum time to wait in milliseconds
* @returns {Promise<void>}
*/
async function waitForAppReady(timeoutMs = 10000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
// Check if app is responsive by looking for a basic UI element
// Try server screen first, then channel list screen
try {
await waitFor(element(by.id('server.screen'))).toBeVisible().withTimeout(2000);
} catch {
await waitFor(element(by.id('channel_list.screen'))).toBeVisible().withTimeout(2000);
}
console.info('✅ App is ready');
return;
} catch {
// App not ready yet, wait a bit
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
throw new Error(`App failed to become ready within ${timeoutMs}ms`);
}
/** /**
* Launch the app with retry mechanism * Launch the app with retry mechanism
* @returns {Promise<void>} * @returns {Promise<void>}
@ -123,11 +176,25 @@ beforeAll(async () => {
await User.apiAdminLogin(siteOneUrl); await User.apiAdminLogin(siteOneUrl);
await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl); await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl);
await launchAppWithRetry(); await launchAppWithRetry();
// Verify Detox connection is healthy after app launch
await verifyDetoxConnection();
// Wait for app to be fully ready (database initialized, bridge ready)
await waitForAppReady();
}); });
// Add this to speed up test cleanup // Add this to speed up test cleanup
afterAll(async () => { afterAll(async () => {
try { try {
// Dismiss keyboard if visible to prevent session invalidation issues
try {
await device.sendToHome();
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
console.warn('[Teardown] Could not send app to home:', error);
}
await device.terminateApp(); await device.terminateApp();
} catch (error) { } catch (error) {
console.error('[Teardown] Error terminating app:', error); console.error('[Teardown] Error terminating app:', error);

116
detox/package-lock.json generated
View file

@ -19,8 +19,8 @@
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/tough-cookie": "4.0.5", "@types/tough-cookie": "4.0.5",
"@types/uuid": "10.0.0", "@types/uuid": "10.0.0",
"@wix-pilot/core": "^3.4.0", "@wix-pilot/core": "3.4.2",
"@wix-pilot/detox": "^1.0.12", "@wix-pilot/detox": "1.0.13",
"async": "3.2.6", "async": "3.2.6",
"axios": "1.8.2", "axios": "1.8.2",
"axios-cookiejar-support": "5.0.4", "axios-cookiejar-support": "5.0.4",
@ -28,7 +28,7 @@
"babel-plugin-module-resolver": "5.0.2", "babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3", "client-oauth2": "4.3.3",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"detox": "20.37.0", "detox": "20.46.3",
"dotenv": "17.2.1", "dotenv": "17.2.1",
"form-data": "4.0.1", "form-data": "4.0.1",
"jest": "29.7.0", "jest": "29.7.0",
@ -2835,9 +2835,9 @@
} }
}, },
"node_modules/@flatten-js/interval-tree": { "node_modules/@flatten-js/interval-tree": {
"version": "1.1.3", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.4.tgz",
"integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==", "integrity": "sha512-o4emRDDvGdkwX18BSVSXH8q27qAL7Z2WDHSN75C8xyRSE4A8UOkig0mWSGoT5M5KaTHZxoLmalFwOTQmbRusUg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -4334,9 +4334,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@wix-pilot/core": { "node_modules/@wix-pilot/core": {
"version": "3.4.0", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@wix-pilot/core/-/core-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@wix-pilot/core/-/core-3.4.2.tgz",
"integrity": "sha512-TH5iH0BISSkMSVBSRiTnh4oYEzlA4gErGiI14US+Ze17uSrWXyrcb4GBpjIuAFMvM3/UbmQBkEg2hXGcl9aDZQ==", "integrity": "sha512-O8V2NLfPEKI2IviJXG4g/vNbMfsBZNBhzAKFUOOaOR9TTDUJYlZUttqjhjP/fPnaDky0/hrfGES15sO0N7zEkw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4354,12 +4354,12 @@
} }
}, },
"node_modules/@wix-pilot/detox": { "node_modules/@wix-pilot/detox": {
"version": "1.0.12", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/@wix-pilot/detox/-/detox-1.0.12.tgz", "resolved": "https://registry.npmjs.org/@wix-pilot/detox/-/detox-1.0.13.tgz",
"integrity": "sha512-yMf9aUv7D+5rViGd7714P06bIgmcJklFFz4/Vjji6TH1gyChX+I/OwP0iK7L5Tob96bPTB9ApM6gR6Je66oIOA==", "integrity": "sha512-/34lM25AfmHNMLOeEIhfKVnx2YZyn5VHC/R4Bs1uXQ2B+Yg0JbxAfvfXA3xqxBsdYqrYImILPO3Ih0c5UzoNxw==",
"dev": true, "dev": true,
"peerDependencies": { "peerDependencies": {
"@wix-pilot/core": "^3.4.0", "@wix-pilot/core": "^3.4.1",
"detox": ">=20.33.0", "detox": ">=20.33.0",
"expect": "29.x.x || 28.x.x || ^27.2.5" "expect": "29.x.x || 28.x.x || ^27.2.5"
} }
@ -4886,9 +4886,9 @@
} }
}, },
"node_modules/bunyan-debug-stream": { "node_modules/bunyan-debug-stream": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/bunyan-debug-stream/-/bunyan-debug-stream-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bunyan-debug-stream/-/bunyan-debug-stream-3.1.1.tgz",
"integrity": "sha512-VaFYbDVdiSn3ZpdozrjZ8mFpxHXl26t11C1DKRQtbo0EgffqeFNrRLOGIESKVeGEvVu4qMxMSSxzNlSw7oTj7w==", "integrity": "sha512-LfMcz4yKM6s9BP5dfT63Prb5B2hAjReLAfQzLbNQF7qBHtn3P1v+/yn0SZ6UAr4PC3VZRX/QzK7HYkkY0ytokQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4899,6 +4899,11 @@
}, },
"peerDependencies": { "peerDependencies": {
"bunyan": "*" "bunyan": "*"
},
"peerDependenciesMeta": {
"bunyan": {
"optional": true
}
} }
}, },
"node_modules/byte-length": { "node_modules/byte-length": {
@ -5329,15 +5334,15 @@
} }
}, },
"node_modules/detox": { "node_modules/detox": {
"version": "20.37.0", "version": "20.46.3",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.37.0.tgz", "resolved": "https://registry.npmjs.org/detox/-/detox-20.46.3.tgz",
"integrity": "sha512-OUI2p3z31Yku1USBia0jrlwoQoa6rANvVWuUSJyjBlRRKDSeOf/DG/ivk+R5FO+lYZZtA3pIbl6LSql/Bf6DpQ==", "integrity": "sha512-9NlpWFu2/CHEMSkzdPcQ9XCGKAtRca2ZLIcVeQDjZvfqkcDOdBt7nj8SX2TNjB4oR6+5MsBg9m0ngQTm8RpNbQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@wix-pilot/core": "^3.2.2", "@wix-pilot/core": "^3.4.2",
"@wix-pilot/detox": "^1.0.11", "@wix-pilot/detox": "^1.0.13",
"ajv": "^8.6.3", "ajv": "^8.6.3",
"bunyan": "^1.8.12", "bunyan": "^1.8.12",
"bunyan-debug-stream": "^3.1.0", "bunyan-debug-stream": "^3.1.0",
@ -5349,7 +5354,7 @@
"funpermaproxy": "^1.1.0", "funpermaproxy": "^1.1.0",
"glob": "^8.0.3", "glob": "^8.0.3",
"ini": "^1.3.4", "ini": "^1.3.4",
"jest-environment-emit": "^1.0.8", "jest-environment-emit": "^1.2.0",
"json-cycle": "^1.3.0", "json-cycle": "^1.3.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"multi-sort-stream": "^1.0.3", "multi-sort-stream": "^1.0.3",
@ -5366,7 +5371,7 @@
"stream-json": "^1.7.4", "stream-json": "^1.7.4",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
"telnet-client": "1.2.8", "telnet-client": "1.2.8",
"tempfile": "^2.0.0", "tmp": "^0.2.1",
"trace-event-lib": "^1.3.1", "trace-event-lib": "^1.3.1",
"which": "^1.3.1", "which": "^1.3.1",
"ws": "^7.0.0", "ws": "^7.0.0",
@ -5381,7 +5386,7 @@
"node": ">=14" "node": ">=14"
}, },
"peerDependencies": { "peerDependencies": {
"jest": "29.x.x || 28.x.x || ^27.2.5" "jest": "30.x.x || 29.x.x || 28.x.x || ^27.2.5"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"jest": { "jest": {
@ -6819,9 +6824,9 @@
} }
}, },
"node_modules/jest-environment-emit": { "node_modules/jest-environment-emit": {
"version": "1.0.8", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/jest-environment-emit/-/jest-environment-emit-1.0.8.tgz", "resolved": "https://registry.npmjs.org/jest-environment-emit/-/jest-environment-emit-1.2.0.tgz",
"integrity": "sha512-WNqvxBLH0yNojHJQ99Y21963aT7UTavxV3PgiBQFi8zwrlnKU6HvkB6LOvQrbk5I8mI8JEKvcoOrQOvBVMLIXQ==", "integrity": "sha512-dSFBrRuIiWbHK2LSUA6CutXpMcNGjjuhvxFLF+TVz5tYFAAH0eesrZgrQ3UtOptajDYNt/fIGRqtlHqGq/bLbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -7902,9 +7907,9 @@
} }
}, },
"node_modules/nan": { "node_modules/nan": {
"version": "2.22.0", "version": "2.24.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
@ -8896,9 +8901,9 @@
} }
}, },
"node_modules/rimraf/node_modules/brace-expansion": { "node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -9440,41 +9445,6 @@
"url": "https://paypal.me/kozjak" "url": "https://paypal.me/kozjak"
} }
}, },
"node_modules/temp-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
"integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/tempfile": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz",
"integrity": "sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"temp-dir": "^1.0.0",
"uuid": "^3.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tempfile/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/test-exclude": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -9569,6 +9539,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",

View file

@ -14,8 +14,8 @@
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/tough-cookie": "4.0.5", "@types/tough-cookie": "4.0.5",
"@types/uuid": "10.0.0", "@types/uuid": "10.0.0",
"@wix-pilot/core": "^3.4.0", "@wix-pilot/core": "3.4.2",
"@wix-pilot/detox": "^1.0.12", "@wix-pilot/detox": "1.0.13",
"async": "3.2.6", "async": "3.2.6",
"axios": "1.8.2", "axios": "1.8.2",
"axios-cookiejar-support": "5.0.4", "axios-cookiejar-support": "5.0.4",
@ -23,7 +23,7 @@
"babel-plugin-module-resolver": "5.0.2", "babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3", "client-oauth2": "4.3.3",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"detox": "20.37.0", "detox": "20.46.3",
"dotenv": "17.2.1", "dotenv": "17.2.1",
"form-data": "4.0.1", "form-data": "4.0.1",
"jest": "29.7.0", "jest": "29.7.0",
@ -55,6 +55,7 @@
"e2e:ios-test": "IOS=true detox test -c ios.sim.debug --reuse --record-logs failing --take-screenshots failing", "e2e:ios-test": "IOS=true detox test -c ios.sim.debug --reuse --record-logs failing --take-screenshots failing",
"e2e:ios-build-release": "detox build -c ios.sim.release", "e2e:ios-build-release": "detox build -c ios.sim.release",
"e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing", "e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing",
"e2e:ios-disable-autofill": "cd utils && node disable_ios_autofill.js",
"detox:config-gen": "cd utils && node generate_detox_config_ci.js", "detox:config-gen": "cd utils && node generate_detox_config_ci.js",
"check": "npm run lint && npm run tsc", "check": "npm run lint && npm run tsc",
"clean-detox": "detox clean", "clean-detox": "detox clean",

View file

@ -0,0 +1,324 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
const os = require('os');
const path = require('path');
const shell = require('shelljs');
// Parse command line arguments
const args = process.argv.slice(2);
let simulatorId = null;
for (let i = 0; i < args.length; i++) {
if ((args[i] === '--simulator-id' || args[i] === '-s') && args[i + 1]) {
simulatorId = args[i + 1];
break;
}
}
function getSimulators() {
const result = shell.exec('xcrun simctl list devices', {silent: true});
if (result.code !== 0) {
console.error('Error: Failed to list iOS simulators');
console.error('Make sure Xcode is installed and xcrun is available');
process.exit(1);
}
const simulators = [];
const lines = result.stdout.split('\n');
let currentOS = '';
for (const line of lines) {
// Match iOS version headers like "-- iOS 17.2 --"
const osMatch = line.match(/-- (iOS [0-9.]+) --/);
if (osMatch) {
currentOS = osMatch[1];
continue;
}
// Match simulator lines like " iPhone 15 Pro (A9A6D652-D75B-4C3A-9CD4-C6BA5E76C6F4) (Shutdown)"
const simMatch = line.match(/^\s+(.+?)\s+\(([A-F0-9-]{36})\)\s+\((Booted|Shutdown|Creating|Booting)\)/);
if (simMatch && currentOS) {
simulators.push({
name: simMatch[1],
udid: simMatch[2],
state: simMatch[3],
os: currentOS,
});
}
}
return simulators;
}
function disablePasswordAutofill(udid, simulator) {
const settingsDir = path.join(
os.homedir(),
'Library/Developer/CoreSimulator/Devices',
udid,
'data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles',
);
const settingsPlist = path.join(settingsDir, 'UserSettings.plist');
console.log(`\nDisabling password autofill for simulator ${udid}...`);
// Check if file exists
if (!shell.test('-f', settingsPlist)) {
console.log('UserSettings.plist not found, creating it...');
// Ensure directory exists
shell.mkdir('-p', settingsDir);
// Create empty plist file
const result = shell.exec(`plutil -create xml1 "${settingsPlist}"`, {silent: true});
if (result.code !== 0) {
console.error(`Error: Failed to create UserSettings.plist at ${settingsPlist}`);
console.error(result.stderr);
return false;
}
}
// Define all keys to set
const keysToSet = [
{
path: 'restrictedBool.allowPasswordAutoFill.value',
type: 'bool',
value: 'NO',
description: 'allowPasswordAutoFill',
},
{
path: 'restrictedBool.allowPasswordAutoFillWithStrongPassword.value',
type: 'bool',
value: 'NO',
description: 'allowPasswordAutoFillWithStrongPassword (iOS 26+)',
},
{
path: 'restrictedBool.forceDisablePasswordAutoFill.value',
type: 'bool',
value: 'YES',
description: 'forceDisablePasswordAutoFill',
},
{
path: 'restrictedBool.allowiCloudKeychain.value',
type: 'bool',
value: 'NO',
description: 'allowiCloudKeychain',
},
{
path: 'restrictedValue.passwordAutoFillPasswords.value',
type: 'integer',
value: '0',
description: 'passwordAutoFillPasswords',
},
{
path: 'restrictedValue.allowAutoFillAuthenticationUI.value',
type: 'integer',
value: '0',
description: 'allowAutoFillAuthenticationUI',
},
];
let successCount = 0;
for (const key of keysToSet) {
console.log(`Setting ${key.description}...`);
// Try to replace the value directly first
let result = shell.exec(
`plutil -replace ${key.path} -${key.type} ${key.value} "${settingsPlist}"`,
{silent: true},
);
if (result.code === 0) {
successCount++;
continue;
}
// Direct replace failed, need to create the key structure
const pathParts = key.path.split('.');
const rootKey = pathParts[0]; // restrictedBool or restrictedValue
const middleKey = pathParts[1]; // e.g., allowPasswordAutoFill
// Ensure root dictionary exists (restrictedBool or restrictedValue)
const checkRoot = shell.exec(
`plutil -extract ${rootKey} xml1 -o - "${settingsPlist}" 2>/dev/null`,
{silent: true},
);
if (checkRoot.code !== 0) {
result = shell.exec(`plutil -insert ${rootKey} -dictionary "${settingsPlist}"`, {silent: true});
if (result.code !== 0) {
console.error(`⚠️ Failed to insert ${rootKey} dictionary`);
console.error(result.stderr);
continue;
}
}
// Ensure middle dictionary exists (e.g., allowPasswordAutoFill)
const checkMiddle = shell.exec(
`plutil -extract ${rootKey}.${middleKey} xml1 -o - "${settingsPlist}" 2>/dev/null`,
{silent: true},
);
if (checkMiddle.code !== 0) {
result = shell.exec(
`plutil -insert ${rootKey}.${middleKey} -dictionary "${settingsPlist}"`,
{silent: true},
);
if (result.code !== 0) {
console.error(`⚠️ Failed to insert ${rootKey}.${middleKey} dictionary`);
console.error(result.stderr);
continue;
}
}
// Set the value (insert or replace)
const checkValue = shell.exec(
`plutil -extract ${key.path} xml1 -o - "${settingsPlist}" 2>/dev/null`,
{silent: true},
);
if (checkValue.code === 0) {
// Value exists, replace it
result = shell.exec(
`plutil -replace ${key.path} -${key.type} ${key.value} "${settingsPlist}"`,
{silent: true},
);
} else {
// Value doesn't exist, insert it
result = shell.exec(
`plutil -insert ${key.path} -${key.type} ${key.value} "${settingsPlist}"`,
{silent: true},
);
}
if (result.code === 0) {
successCount++;
} else {
console.error(`⚠️ Failed to set ${key.description}`);
console.error(result.stderr);
}
}
// Verify that all keys were actually written to the plist
console.log('\nVerifying restriction keys...');
let verifiedCount = 0;
for (const key of keysToSet) {
const checkResult = shell.exec(
`plutil -extract ${key.path} xml1 -o - "${settingsPlist}" 2>/dev/null`,
{silent: true},
);
if (checkResult.code === 0) {
// Extract just the value for logging
const valueMatch = checkResult.stdout.match(/<(true|false|integer)>([^<]*)<\/(true|false|integer)>/);
if (valueMatch) {
console.log(`${key.description}: ${valueMatch[0]}`);
verifiedCount++;
}
} else {
console.log(`${key.description}: MISSING or FAILED`);
}
}
if (verifiedCount === keysToSet.length) {
console.log(`\n✅ All ${keysToSet.length} restriction keys verified in plist`);
} else {
console.error(`\n⚠️ Only ${verifiedCount}/${keysToSet.length} keys verified`);
}
if (successCount === keysToSet.length) {
console.log(`✅ Password autofill disabled successfully (${keysToSet.length} restriction keys set)`);
return true;
} else if (successCount > 0) {
console.log(`⚠️ Partially successful: ${successCount}/${keysToSet.length} keys set`);
// Check if this is iOS 26+
const isIOS26Plus = simulator && simulator.os &&
parseFloat(simulator.os.replace('iOS ', '')) >= 26;
if (isIOS26Plus) {
console.error('⚠️ CRITICAL: iOS 26+ requires ALL 6 keys to fully disable password autofill');
console.error(' Missing keys may allow "Strong Password" or iCloud Keychain prompts');
}
return false;
}
console.error('⚠️ Failed to disable password autofill');
return false;
}
async function main() {
console.log('iOS Simulator - Disable Password Autofill\n');
console.log('This tool disables password autofill on iOS simulators,');
console.log('which can interfere with Detox E2E test login flows.');
console.log('Sets 6 iOS restriction keys to fully disable autofill prompts.\n');
// Check if running on macOS
if (process.platform !== 'darwin') {
console.error('Error: This tool only works on macOS');
process.exit(1);
}
// Check if xcrun is available
if (!shell.which('xcrun')) {
console.error('Error: xcrun not found. Please install Xcode and command line tools');
process.exit(1);
}
// Get list of simulators
const simulators = getSimulators();
if (simulators.length === 0) {
console.error('Error: No iOS simulators found');
console.error('Create a simulator in Xcode first');
process.exit(1);
}
let selectedSimulator;
if (simulatorId) {
// Use provided simulator ID
selectedSimulator = simulators.find((sim) => sim.udid === simulatorId);
if (!selectedSimulator) {
console.error(`Error: Simulator with ID ${simulatorId} not found`);
process.exit(1);
}
console.log(`Target: ${selectedSimulator.name} (${selectedSimulator.os})`);
console.log(`Using simulator: ${selectedSimulator.name} (${selectedSimulator.os})`);
} else {
// Automatically select iPhone 17 Pro with iOS 26.2
selectedSimulator = simulators.find((sim) =>
sim.name === 'iPhone 17 Pro' &&
sim.os === 'iOS 26.2',
);
if (!selectedSimulator) {
console.error('Error: iPhone 17 Pro (iOS 26.2) simulator not found');
console.error('Please create this simulator in Xcode first.');
console.error('\nAvailable simulators:');
simulators.forEach((sim) => {
const stateIndicator = sim.state === 'Booted' ? '🟢' : '⚪';
console.error(` ${stateIndicator} ${sim.name} (${sim.os}) - ${sim.state}`);
});
process.exit(1);
}
console.log(`Target: ${selectedSimulator.name} (${selectedSimulator.os})`);
console.log(`Using simulator: ${selectedSimulator.name} (${selectedSimulator.os})`);
}
// Apply the setting
const success = disablePasswordAutofill(selectedSimulator.udid, selectedSimulator);
process.exit(success ? 0 : 1);
}
main();

View file

@ -4,8 +4,8 @@
/* eslint-disable no-process-env */ /* eslint-disable no-process-env */
/* eslint-disable no-console */ /* eslint-disable no-console */
const fs = require('fs'); const fs = require('fs');
const deviceName = process.env.DEVICE_NAME || 'iPhone 15 Pro'; const deviceName = process.env.DEVICE_NAME || 'iPhone 17 Pro';
const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 17.2'; const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 26.2';
const detoxConfigTemplate = fs.readFileSync('../.detoxrc.json', 'utf8'); const detoxConfigTemplate = fs.readFileSync('../.detoxrc.json', 'utf8');
const detoxConfig = detoxConfigTemplate.replace('__DEVICE_NAME__', deviceName).replace('__DEVICE_OS_VERSION__', deviceOSVersion); const detoxConfig = detoxConfigTemplate.replace('__DEVICE_NAME__', deviceName).replace('__DEVICE_OS_VERSION__', deviceOSVersion);

View file

@ -319,7 +319,7 @@ lane :upload_file_to_s3 do |options|
}) })
end end
if options[:file] == 'Mattermost-simulator-x86_64.app.zip' if options[:file]&.start_with?('Mattermost-simulator-') && options[:file]&.end_with?('.app.zip')
pretext = '#### New iOS build for VM/Simulator' pretext = '#### New iOS build for VM/Simulator'
msg = "Download link: #{links.first}" msg = "Download link: #{links.first}"
send_message_to_mattermost({ send_message_to_mattermost({
@ -419,10 +419,22 @@ platform :ios do
lane :simulator do lane :simulator do
UI.success('Building iOS app for simulator') UI.success('Building iOS app for simulator')
output_file = "Mattermost-simulator-x86_64.app.zip" # Detect the host architecture
host_arch = `uname -m`.strip
# Use arm64 for Apple Silicon (arm64), x86_64 for Intel
if host_arch == "arm64"
build_arch = "arm64"
output_file = "Mattermost-simulator-arm64.app.zip"
else
build_arch = "x86_64"
output_file = "Mattermost-simulator-x86_64.app.zip"
end
UI.message("Building for #{build_arch} architecture on #{host_arch} host")
build_ios_unsigned_or_simulator( build_ios_unsigned_or_simulator(
more_xc_args: "-sdk iphonesimulator -arch x86_64", more_xc_args: "-sdk iphonesimulator -arch #{build_arch}",
rel_build_dir: "Release-iphonesimulator", rel_build_dir: "Release-iphonesimulator",
output_file: output_file, output_file: output_file,
) )