chore(e2e): Change iOS simulator to 26.1 from 17.2 (#9370)
This commit is contained in:
parent
d014856a3a
commit
fdc559d70d
14 changed files with 745 additions and 99 deletions
8
.github/workflows/e2e-detox-pr.yml
vendored
8
.github/workflows/e2e-detox-pr.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
172
.github/workflows/e2e-ios-template.yml
vendored
172
.github/workflows/e2e-ios-template.yml
vendored
|
|
@ -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: |
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
116
detox/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
324
detox/utils/disable_ios_autofill.js
Normal file
324
detox/utils/disable_ios_autofill.js
Normal 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();
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue