diff --git a/.github/workflows/debug-docker.yml b/.github/workflows/debug-docker.yml new file mode 100644 index 00000000..a600392e --- /dev/null +++ b/.github/workflows/debug-docker.yml @@ -0,0 +1,115 @@ +name: Debug Docker Setup + +# on: +# workflow_dispatch: +# pull_request: + +jobs: + debug-docker: + runs-on: macos-13 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check system info + run: | + echo "=== System Information ===" + uname -a + sw_vers + echo "=== Available commands ===" + which brew + which python3 + which docker || echo "Docker not found" + + - name: Install Docker CLI only + run: | + echo "=== Installing Docker CLI (no GUI) ===" + # Install just the Docker CLI, not the full Desktop app + brew install docker + echo "Docker CLI installed" + + - name: Set up Docker + uses: docker/setup-docker-action@v4 + id: docker-official + continue-on-error: true + with: + install: false # We already installed it + daemon-config: | + { + "experimental": false, + "debug": true, + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } + } + + # - name: Setup Docker Colima (Fallback) + # uses: douglascamata/setup-docker-macos-action@v1-alpha + # id: docker-colima + # continue-on-error: true + + - name: Check Docker setup results + run: | + echo "=== Docker Setup Results ===" + echo "Official Docker action result: ${{ steps.docker-official.outcome }}" + # echo "Colima action result: ${{ steps.docker-colima.outcome }}" + + - name: Wait for Docker to start + run: | + echo "=== Waiting for Docker to start ===" + # Use a for loop instead of timeout (macOS doesn't have timeout command) + for i in {1..18}; do + if docker info >/dev/null 2>&1; then + echo "Docker is ready!" + break + fi + echo "Waiting for Docker... ($(date)) (attempt $i/18)" + sleep 10 + done + + # Final check + if ! docker info >/dev/null 2>&1; then + echo "Docker failed to start after 3 minutes" + exit 1 + fi + + - name: Test Docker functionality + run: | + echo "=== Testing Docker ===" + docker --version + docker info + docker ps + echo "=== Testing Docker Compose V2 ===" + docker compose version || echo "docker compose not found" + + - name: Install Docker Compose V2 + run: | + echo "=== Installing Docker Compose V2 ===" + # Install Docker Compose V2 plugin via Homebrew + brew install docker-compose + echo "Docker Compose V2 installed" + docker compose version + + - name: Test simple Docker command + run: | + echo "=== Testing simple Docker command ===" + docker run --rm hello-world + echo "Docker test successful!" + + - name: Test Docker Compose V2 + run: | + echo "=== Testing Docker Compose V2 ===" + mkdir -p test-compose + cat > test-compose/docker-compose.yml << 'EOF' + version: '3.8' + services: + test: + image: hello-world + command: echo "Docker Compose V2 test successful!" + EOF + cd test-compose + docker compose up + echo "Docker Compose V2 test successful!" diff --git a/.github/workflows/e2e-self-hosted.yml b/.github/workflows/e2e-self-hosted.yml new file mode 100644 index 00000000..79b5b302 --- /dev/null +++ b/.github/workflows/e2e-self-hosted.yml @@ -0,0 +1,243 @@ +name: e2e-tests + +on: + workflow_dispatch: + inputs: + e2e_branch: + description: "Branch of synonymdev/bitkit-e2e-tests to use" + required: false + default: "main" + pull_request: + +env: + TERM: xterm-256color + FORCE_COLOR: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: self-hosted + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # - name: Setup Xcode + # uses: maxim-lobanov/setup-xcode@v1 + # with: + # xcode-version: '16.4' + + - name: Setup iOS Simulator + run: | + # Set simulator name (easy to change if needed) + SIMULATOR_NAME="iPhone 16" + echo "SIMULATOR_NAME=$SIMULATOR_NAME" >> $GITHUB_ENV + + # List available simulators + xcrun simctl list devices available + + # Kill any existing simulators to start fresh + xcrun simctl shutdown all || true + sleep 5 + + # Boot the specified simulator + echo "Booting $SIMULATOR_NAME..." + xcrun simctl boot "$SIMULATOR_NAME" || true + + # Wait for simulator to boot + echo "Waiting for simulator to boot..." + for i in {1..30}; do + if xcrun simctl list devices | grep "$SIMULATOR_NAME" | grep -q "Booted"; then + echo "$SIMULATOR_NAME is booted!" + break + fi + echo "Waiting for $SIMULATOR_NAME boot... ($i/30)" + sleep 5 + done + + # Additional wait for simulator to be fully ready + sleep 15 + + # Install the app + xcrun simctl install "$SIMULATOR_NAME" e2e-app/bitkit.app + + # Verify app installation + xcrun simctl listapps "$SIMULATOR_NAME" | grep -i bitkit || echo "App not found in simulator" + + # Check simulator status + xcrun simctl list devices | grep "$SIMULATOR_NAME" + + # Launch simulator app to ensure it's visible + open -a Simulator + sleep 5 + + - name: Build iOS app + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHATWOOT_API: ${{ secrets.CHATWOOT_API }} + E2E: true + run: | + xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \ + -scheme Bitkit \ + -configuration Debug \ + -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ + -derivedDataPath DerivedData \ + build + + - name: Prepare app for E2E tests + run: | + # Copy the .app bundle to the expected location and name + mkdir -p e2e-app + cp -r DerivedData/Build/Products/Debug-iphonesimulator/Bitkit.app e2e-app/bitkit.app + + - name: Upload iOS app + uses: actions/upload-artifact@v4 + with: + name: bitkit-e2e-ios_${{ github.run_number }} + path: e2e-app/ + + # e2e-tests: + # runs-on: self-hosted + # needs: build + + # strategy: + # fail-fast: false + # matrix: + # shard: + # # - { name: onboarding_backup_numberpad, grep: "@onboarding|@backup|@numberpad" } + # # - { name: onchain_boost_receive_widgets, grep: "@onchain|@boost|@receive|@widgets" } + # # - { name: settings, grep: "@settings" } + # # - { name: security, grep: "@security" } + # - { name: onboarding, grep: "@onboarding" } + + # name: e2e-tests - ${{ matrix.shard.name }} + + # steps: + # - name: Show selected E2E branch + # env: + # E2E_BRANCH: ${{ github.event.inputs.e2e_branch || 'main' }} + # run: echo $E2E_BRANCH + + # - name: Clone E2E tests + # uses: actions/checkout@v4 + # with: + # repository: synonymdev/bitkit-e2e-tests + # path: bitkit-e2e-tests + # ref: ${{ github.event.inputs.e2e_branch || 'main' }} + + # - name: Download iOS app + # uses: actions/download-artifact@v4 + # with: + # name: bitkit-e2e-ios_${{ github.run_number }} + # path: bitkit-e2e-tests/aut + + # - name: List iOS app directory contents + # run: ls -l bitkit-e2e-tests/aut + + # - name: Setup Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: 22 + + # - name: Cache npm cache + # uses: actions/cache@v3 + # with: + # path: ~/.npm + # key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + # restore-keys: | + # ${{ runner.os }}-node- + + # - name: Install dependencies + # working-directory: bitkit-e2e-tests + # run: npm ci + + # - name: Install Docker + # run: | + # # Install Docker Desktop for Mac + # brew install --cask docker + # # Start Docker Desktop + # open -a Docker + # # Wait for Docker to be ready (with longer timeout) + # echo "Waiting for Docker to start..." + # timeout 120 bash -c 'until docker info >/dev/null 2>&1; do echo "Waiting for Docker..."; sleep 5; done' + # echo "Docker is ready!" + + # - name: Install Docker Compose + # run: | + # # Install docker-compose via pip (more reliable than brew on macOS runners) + # python3 -m pip install docker-compose + + # - name: Docker info + # run: | + # docker --version + # docker info + # docker ps + + # - name: Run regtest setup + # working-directory: bitkit-e2e-tests + # run: | + # cd docker + # mkdir lnd && chmod 777 lnd + # docker compose pull --quiet + # docker compose up -d + + # - name: Wait for electrum server and LND + # working-directory: bitkit-e2e-tests + # timeout-minutes: 10 + # run: | + # while ! nc -z '127.0.0.1' 60001; do sleep 1; done + # sudo bash -c "while [ ! -f docker/lnd/data/chain/bitcoin/regtest/admin.macaroon ]; do sleep 1; done" + # sudo chmod -R 777 docker/lnd + + # - name: Setup iOS Simulator + # run: | + # # Boot iOS Simulator + # xcrun simctl boot "iPhone 16" || true + # xcrun simctl bootstatus "iPhone 16" -b + + # # Install the app + # xcrun simctl install "iPhone 16" bitkit-e2e-tests/aut/bitkit.app + + # - name: Run E2E Tests (${{ matrix.shard.name }}) + # run: | + # cd bitkit-e2e-tests + + # # Setup logging + # LOGDIR="./artifacts" + # mkdir -p "$LOGDIR" + # LOGFILE="$LOGDIR/simulator.log" + + # # Start simulator logging + # xcrun simctl spawn "iPhone 16" log stream --predicate 'process == "Bitkit"' --style compact > "$LOGFILE" & + # LOG_PID=$! + + # # Setup port forwarding for regtest and LND + # xcrun simctl spawn "iPhone 16" launchctl load -w /System/Library/LaunchDaemons/com.apple.usbmuxd.plist || true + + # # Cleanup function + # cleanup() { + # kill "$LOG_PID" 2>/dev/null || true + # wait "$LOG_PID" 2>/dev/null || true + # } + # trap cleanup EXIT INT TERM + + # # Pass everything through to WDIO/Mocha + # npm run e2e:ios -- "$@" + # env: + # RECORD_VIDEO: true + + # - name: Upload E2E Artifacts (${{ matrix.shard.name }}) + # if: failure() + # uses: actions/upload-artifact@v4 + # with: + # name: e2e-artifacts_${{ matrix.shard.name }}_${{ github.run_number }} + # path: bitkit-e2e-tests/artifacts/ + + # - name: Dump docker logs on failure (${{ matrix.shard.name }}) + # if: failure() + # uses: jwalton/gh-docker-logs@v2 + \ No newline at end of file diff --git a/.github/workflows/e2e-tests-arm.yml b/.github/workflows/e2e-tests-arm.yml new file mode 100644 index 00000000..3ed6fe4c --- /dev/null +++ b/.github/workflows/e2e-tests-arm.yml @@ -0,0 +1,85 @@ +name: e2e-tests-arm + +# on: +# workflow_dispatch: +# pull_request: + +jobs: + docker: + runs-on: macos-26 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check system info + run: | + echo "=== System Information ===" + uname -a + sw_vers + echo "=== Available commands ===" + which brew + which docker || echo "Docker not found" + + - name: Install Docker (via Colima) + run: | + brew install colima docker + + # Check available VM types + echo "Available VM types:" + colima start --help | grep -A 10 "vm-type" || echo "Help not available" + + echo "Trying VZ VM type..." + if ! colima start --arch aarch64 --vm-type=vz --memory 4 --cpu 2; then + echo "Direct start failed, trying brew services approach..." + brew services start colima + sleep 10 + fi + + - name: Test Docker + run: | + echo "=== Colima Status ===" + colima status || echo "Colima not running" + + echo "=== Colima Configuration ===" + colima list || echo "No Colima instances" + + echo "=== Colima Logs (startup) ===" + if [ -f "/Users/runner/.colima/_lima/colima/ha.stderr.log" ]; then + echo "Host agent stderr:" + tail -20 "/Users/runner/.colima/_lima/colima/ha.stderr.log" + else + echo "No stderr log found" + fi + + if [ -f "/Users/runner/.colima/_lima/colima/ha.stdout.log" ]; then + echo "Host agent stdout:" + tail -20 "/Users/runner/.colima/_lima/colima/ha.stdout.log" + else + echo "No stdout log found" + fi + + echo "=== System Virtualization Support ===" + sysctl kern.hv_support || echo "No HV support" + sysctl kern.vm_guest || echo "No VM guest info" + + echo "=== Brew Services Status ===" + brew services list | grep colima || echo "No colima service found" + + echo "=== Trying to start Colima manually with VZ ===" + colima start --vm-type=vz --memory 2 --cpu 1 || echo "VZ manual start failed" + + echo "=== Final Colima Status ===" + colima status || echo "Colima still not running" + + echo "=== CONCLUSION ===" + echo "โŒ VZ virtualization is not available on this GitHub Actions runner" + echo "The error shows: 'Virtualization is not available on this hardware'" + echo "This means we cannot run Docker on the same runner as the iOS simulator" + echo "" + echo "๐Ÿ”ง SOLUTIONS:" + echo "1. Use the hybrid approach (Ubuntu for regtest, macOS for iOS tests)" + echo "2. Use a cloud-hosted regtest service" + echo "3. Use a different CI provider that supports virtualization" + echo "" + echo "The hybrid approach in e2e-tests-hybrid.yml is the recommended solution" diff --git a/.github/workflows/e2e-tests-hybrid.yml b/.github/workflows/e2e-tests-hybrid.yml new file mode 100644 index 00000000..ed7f11cc --- /dev/null +++ b/.github/workflows/e2e-tests-hybrid.yml @@ -0,0 +1,577 @@ +name: e2e-tests-hybrid + +# on: +# workflow_dispatch: +# pull_request: + +env: + TERM: xterm-256color + FORCE_COLOR: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Job 1: Run regtest infrastructure on Ubuntu with tunneling + regtest-infrastructure: + runs-on: ubuntu-latest + outputs: + electrum-url: ${{ env.ELECTRUM_URL }} + lnd-url: ${{ env.LND_URL }} + + steps: + - name: Clone E2E tests + uses: actions/checkout@v4 + with: + repository: synonymdev/bitkit-e2e-tests + path: bitkit-e2e-tests + ref: ${{ github.event.inputs.e2e_branch || 'ios' }} + + - name: Run regtest setup + working-directory: bitkit-e2e-tests + id: setup-regtest + run: | + cd docker + mkdir lnd && chmod 777 lnd + docker compose pull --quiet + docker compose up -d + + echo "Waiting for electrum server..." + while ! nc -z '127.0.0.1' 60001; do sleep 1; done + echo "Electrum server is ready!" + + echo "Waiting for LND to initialize..." + # Wait for LND container to be running + timeout 60 bash -c 'until docker ps | grep lnd | grep -q "Up"; do echo "Waiting for LND container..."; sleep 2; done' + + # Fix permissions for LND data directory + echo "Fixing LND data directory permissions..." + sudo chown -R runner:runner lnd/ + chmod -R 755 lnd/ + + # Check if LND directory exists and wait for admin.macaroon + echo "Checking LND data directory..." + ls -la lnd/ || echo "LND directory not found" + ls -la lnd/data/ || echo "LND data directory not found" + ls -la lnd/data/chain/ || echo "LND chain directory not found" + ls -la lnd/data/chain/bitcoin/ || echo "LND bitcoin directory not found" + ls -la lnd/data/chain/bitcoin/regtest/ || echo "LND regtest directory not found" + + # Wait for admin.macaroon with timeout and better error handling + echo "Waiting for admin.macaroon..." + timeout 120 bash -c 'until [ -f lnd/data/chain/bitcoin/regtest/admin.macaroon ]; do echo "Waiting for admin.macaroon... ($(date))"; sleep 5; done' || { + echo "Timeout waiting for admin.macaroon. Checking LND logs:" + docker logs lnd + echo "LND directory contents:" + find lnd -name "*.macaroon" -type f 2>/dev/null || echo "No macaroon files found" + exit 1 + } + + echo "Admin macaroon found! Setting permissions..." + chmod -R 777 lnd + + echo "Regtest infrastructure ready locally" + + - name: Setup cloudflared + uses: AnimMouse/setup-cloudflared@v2 + + - name: Expose Electrum with Cloudflare Tunnel + id: tunnel-electrum + run: | + echo "=== Creating Electrum tunnel ===" + # Create tunnel manually using cloudflared + nohup cloudflared tunnel --url http://localhost:60001 > electrum-tunnel.log 2>&1 & + ELECTRUM_TUNNEL_PID=$! + echo "ELECTRUM_TUNNEL_PID=$ELECTRUM_TUNNEL_PID" >> $GITHUB_ENV + echo "Electrum tunnel PID: $ELECTRUM_TUNNEL_PID" + + # Wait for tunnel to be ready and extract URL + sleep 10 + if [ -f electrum-tunnel.log ]; then + ELECTRUM_URL=$(grep -o 'https://[^[:space:]]*\.trycloudflare\.com' electrum-tunnel.log | head -1) + if [ -n "$ELECTRUM_URL" ]; then + echo "Electrum tunnel URL: $ELECTRUM_URL" + echo "ELECTRUM_URL=$ELECTRUM_URL" >> $GITHUB_ENV + else + echo "Failed to extract Electrum tunnel URL" + cat electrum-tunnel.log + fi + fi + continue-on-error: true + + - name: Expose LND with Cloudflare Tunnel + id: tunnel-lnd + run: | + echo "=== Creating LND tunnel ===" + # Create tunnel manually using cloudflared + nohup cloudflared tunnel --url http://localhost:9735 > lnd-tunnel.log 2>&1 & + LND_TUNNEL_PID=$! + echo "LND_TUNNEL_PID=$LND_TUNNEL_PID" >> $GITHUB_ENV + echo "LND tunnel PID: $LND_TUNNEL_PID" + + # Wait for tunnel to be ready and extract URL + sleep 10 + if [ -f lnd-tunnel.log ]; then + LND_URL=$(grep -o 'https://[^[:space:]]*\.trycloudflare\.com' lnd-tunnel.log | head -1) + if [ -n "$LND_URL" ]; then + echo "LND tunnel URL: $LND_URL" + echo "LND_URL=$LND_URL" >> $GITHUB_ENV + else + echo "Failed to extract LND tunnel URL" + cat lnd-tunnel.log + fi + fi + continue-on-error: true + + - name: Keep regtest running + run: | + # Start background processes to keep regtest and tunnels alive + echo "Starting regtest keep-alive process..." + nohup bash -c 'while true; do echo "Regtest infrastructure running... $(date)"; sleep 60; done' & + KEEPALIVE_PID=$! + echo "Keep-alive PID: $KEEPALIVE_PID" + + # Start tunnel keep-alive process + echo "Starting tunnel keep-alive process..." + nohup bash -c 'while true; do echo "Tunnels running... $(date)"; sleep 60; done' & + TUNNEL_PID=$! + echo "Tunnel keep-alive PID: $TUNNEL_PID" + + echo "Regtest infrastructure and tunnels are running in background" + + # Verify the processes are running + sleep 5 + if ps -p $KEEPALIVE_PID > /dev/null; then + echo "โœ… Keep-alive process is running" + else + echo "โŒ Keep-alive process failed to start" + exit 1 + fi + + if ps -p $TUNNEL_PID > /dev/null; then + echo "โœ… Tunnel keep-alive process is running" + else + echo "โŒ Tunnel keep-alive process failed to start" + exit 1 + fi + + # Show what's running + echo "Current processes:" + ps aux | grep -E "(docker|electrum|lnd|keep|tunnel)" | head -10 + + # Test if services are actually accessible + echo "Testing local connectivity..." + if nc -z localhost 60001; then + echo "โœ… Electrum server is accessible locally" + else + echo "โŒ Electrum server is NOT accessible locally" + fi + + if nc -z localhost 9735; then + echo "โœ… LND is accessible locally" + else + echo "โŒ LND is NOT accessible locally" + fi + + # Show Docker containers + echo "Docker containers:" + docker ps + + # Show tunnel URLs and verify they exist + echo "Tunnel URLs:" + echo "Electrum: $ELECTRUM_URL" + echo "LND: $LND_URL" + + # Verify tunnel URLs are not empty + if [ -z "$ELECTRUM_URL" ] || [ -z "$LND_URL" ]; then + echo "โŒ ERROR: Tunnel URLs are empty!" + echo "This means the Cloudflare tunnel setup failed." + echo "Continuing anyway - E2E tests will handle the error..." + else + echo "โœ… Tunnel URLs are available" + fi + + # Job is complete - background processes will keep everything running + echo "โœ… Regtest infrastructure and tunnels are running in background" + echo "Job 1 completed - E2E tests can now start" + + # Job 2: Build iOS app and run E2E tests on macOS + e2e-tests: + runs-on: macos-latest + needs: regtest-infrastructure + + strategy: + fail-fast: false + matrix: + shard: + - { name: onboarding, grep: "@onboarding" } + + name: e2e-tests - ${{ matrix.shard.name }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.4' + + - name: Build iOS app + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHATWOOT_API: ${{ secrets.CHATWOOT_API }} + E2E: true + # Override regtest URLs to point to the Ubuntu runner + E2E_ELECTRUM_SERVER: ${{ needs.regtest-infrastructure.outputs.electrum-url }} + E2E_LND_URL: ${{ needs.regtest-infrastructure.outputs.lnd-url }} + run: | + xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \ + -scheme Bitkit \ + -configuration Debug \ + -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ + -derivedDataPath DerivedData \ + build + + - name: Prepare app for E2E tests + run: | + mkdir -p e2e-app + cp -r DerivedData/Build/Products/Debug-iphonesimulator/Bitkit.app e2e-app/bitkit.app + + - name: Clone E2E tests + uses: actions/checkout@v4 + with: + repository: synonymdev/bitkit-e2e-tests + path: bitkit-e2e-tests + ref: ${{ github.event.inputs.e2e_branch || 'ios' }} + + - name: Copy app to E2E tests directory + run: | + # Create the aut directory in the E2E tests + mkdir -p bitkit-e2e-tests/aut + # Copy the app to the expected location + cp -r e2e-app/bitkit.app bitkit-e2e-tests/aut/bitkit.app + # Verify the app was copied + ls -la bitkit-e2e-tests/aut/ + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + working-directory: bitkit-e2e-tests + run: npm ci + + - name: Install Appium + run: | + # Install Appium globally + npm install -g appium@latest + # Install XCUITest driver for iOS + appium driver install xcuitest + # Verify installation + appium driver list + + - name: Setup iOS Simulator + run: | + # Set simulator name (easy to change if needed) + SIMULATOR_NAME="iPhone 16" + echo "SIMULATOR_NAME=$SIMULATOR_NAME" >> $GITHUB_ENV + + # List available simulators + xcrun simctl list devices available + + # Kill any existing simulators to start fresh + xcrun simctl shutdown all || true + sleep 5 + + # Boot the specified simulator + echo "Booting $SIMULATOR_NAME..." + xcrun simctl boot "$SIMULATOR_NAME" || true + + # Wait for simulator to boot + echo "Waiting for simulator to boot..." + for i in {1..30}; do + if xcrun simctl list devices | grep "$SIMULATOR_NAME" | grep -q "Booted"; then + echo "$SIMULATOR_NAME is booted!" + break + fi + echo "Waiting for $SIMULATOR_NAME boot... ($i/30)" + sleep 5 + done + + # Additional wait for simulator to be fully ready + sleep 15 + + # Install the app + xcrun simctl install "$SIMULATOR_NAME" e2e-app/bitkit.app + + # Verify app installation + xcrun simctl listapps "$SIMULATOR_NAME" | grep -i bitkit || echo "App not found in simulator" + + # Check simulator status + xcrun simctl list devices | grep "$SIMULATOR_NAME" + + # Launch simulator app to ensure it's visible + open -a Simulator + sleep 5 + + - name: Start Appium Server + run: | + # Start Appium server in background with more verbose logging + appium --port 4723 --log-level debug --relaxed-security --session-override & + APPIUM_PID=$! + echo "APPIUM_PID=$APPIUM_PID" >> $GITHUB_ENV + + # Wait for Appium to be ready (macOS doesn't have timeout command) + echo "Waiting for Appium to start..." + for i in {1..30}; do + if curl -s http://localhost:4723/status >/dev/null 2>&1; then + echo "Appium server is ready!" + break + fi + echo "Waiting for Appium... ($i/30)" + sleep 2 + done + + # Final check + if ! curl -s http://localhost:4723/status >/dev/null 2>&1; then + echo "Appium failed to start after 60 seconds" + exit 1 + fi + + # Test Appium with a simple request + echo "Testing Appium connection..." + curl -s http://localhost:4723/status | head -20 + + # Check if simulator is accessible to Appium + echo "Checking simulator accessibility..." + xcrun simctl list devices | grep "iPhone 16" + + # Try to get simulator logs to debug + echo "Recent simulator logs:" + xcrun simctl spawn "iPhone 16" log show --last 1m --predicate 'process == "SpringBoard"' | head -10 + + - name: Run E2E Tests (${{ matrix.shard.name }}) + timeout-minutes: 30 + run: | + cd bitkit-e2e-tests + + # Setup logging + LOGDIR="./artifacts" + mkdir -p "$LOGDIR" + LOGFILE="$LOGDIR/simulator.log" + + # Start simulator logging + xcrun simctl spawn "$SIMULATOR_NAME" log stream --predicate 'process == "Bitkit"' --style compact > "$LOGFILE" & + LOG_PID=$! + + # Cleanup function + cleanup() { + echo "Cleaning up processes..." + kill "$LOG_PID" 2>/dev/null || true + wait "$LOG_PID" 2>/dev/null || true + kill "$APPIUM_PID" 2>/dev/null || true + wait "$APPIUM_PID" 2>/dev/null || true + echo "Cleanup complete" + } + trap cleanup EXIT INT TERM + + # Configure WebDriverIO to use our Appium server + echo "Configuring WebDriverIO to use our Appium server..." + + # Add hostname if missing + if ! grep -q "hostname:" wdio.conf.ts; then + echo "Adding hostname configuration..." + sed -i '' 's/port: 4723,/port: 4723,\n hostname: '\''localhost'\'',/' wdio.conf.ts + fi + + # Remove appium service to use our manually started server + echo "Removing appium service from services array..." + sed -i '' "s/services: \['appium'\]/services: []/" wdio.conf.ts + + # Update timeouts for better reliability + echo "Updating timeout configurations..." + sed -i '' 's/waitforTimeout: 30000/waitforTimeout: 60000/' wdio.conf.ts + sed -i '' 's/connectionRetryTimeout: 120000/connectionRetryTimeout: 300000/' wdio.conf.ts + + # Get the UDID of the booted simulator + SIMULATOR_UDID=$(xcrun simctl list devices | grep "$SIMULATOR_NAME" | grep "Booted" | grep -o '[A-F0-9-]\{36\}') + echo "Booted simulator UDID: $SIMULATOR_UDID" + + # Device name is already set to "iPhone 16" in wdio.conf.ts + echo "Using simulator: $SIMULATOR_NAME (UDID: $SIMULATOR_UDID)" + + # Check the full capabilities section + echo "Full capabilities section:" + grep -A 20 -B 5 "capabilities:" wdio.conf.ts + + # Update UDID to match the booted simulator + echo "Updating UDID to match booted simulator: $SIMULATOR_UDID" + sed -i '' "s/'appium:udid': '[^']*'/'appium:udid': '$SIMULATOR_UDID'/" wdio.conf.ts + + # Verify the UDID was updated + echo "Updated UDID in capabilities:" + grep -A 5 -B 5 "appium:udid" wdio.conf.ts + + # Verify the bundle ID was updated + echo "Updated bundle ID in capabilities:" + grep -A 5 -B 5 "appium:bundleId" wdio.conf.ts + + # Don't manually launch the app - let Appium handle it + # Just ensure simulator is ready and app is installed + echo "Ensuring simulator is ready..." + xcrun simctl list devices | grep "$SIMULATOR_UDID" + + # Verify app is installed + echo "Verifying app installation..." + xcrun simctl listapps "$SIMULATOR_UDID" | grep -i bitkit || echo "App not found" + + # Make sure simulator is fully booted and ready + echo "Waiting for simulator to be fully ready..." + sleep 15 + + # Pre-build WebDriverAgent to avoid timeout during session creation + echo "Pre-building WebDriverAgent..." + xcrun simctl spawn "$SIMULATOR_UDID" xcrun simctl list devices || echo "Simulator check failed" + + # Ensure simulator is fully responsive + echo "Testing simulator responsiveness..." + for i in {1..10}; do + if xcrun simctl list devices | grep "$SIMULATOR_UDID" | grep -q "Booted"; then + echo "Simulator is responsive (attempt $i/10)" + break + fi + echo "Waiting for simulator responsiveness... ($i/10)" + sleep 5 + done + + # Check simulator status one more time + echo "Final simulator status:" + xcrun simctl list devices | grep "$SIMULATOR_UDID" + + # Re-check UDID in case simulator was reset + echo "Re-checking simulator UDID..." + CURRENT_UDID=$(xcrun simctl list devices | grep "$SIMULATOR_NAME" | grep "Booted" | grep -o '[A-F0-9-]\{36\}') + if [ "$CURRENT_UDID" != "$SIMULATOR_UDID" ]; then + echo "UDID changed from $SIMULATOR_UDID to $CURRENT_UDID, updating..." + sed -i '' "s/'appium:udid': '[^']*'/'appium:udid': '$CURRENT_UDID'/" wdio.conf.ts + SIMULATOR_UDID="$CURRENT_UDID" + fi + + # Test regtest connectivity + echo "Testing regtest connectivity..." + echo "E2E_ELECTRUM_SERVER: $E2E_ELECTRUM_SERVER" + echo "E2E_LND_URL: $E2E_LND_URL" + + # Check if we have valid regtest URLs + if [ -z "$E2E_ELECTRUM_SERVER" ] || [ -z "$E2E_LND_URL" ]; then + echo "โŒ ERROR: Regtest URLs are empty!" + echo "This means the regtest-infrastructure job failed or didn't provide outputs." + echo "Check the regtest-infrastructure job logs for errors." + exit 1 + fi + + echo "โœ… Using Cloudflare Tunnel URLs for regtest connectivity" + + # Test electrum server connectivity via tunnel + if [ -n "$E2E_ELECTRUM_SERVER" ]; then + echo "Testing electrum server at $E2E_ELECTRUM_SERVER..." + + # Test with curl (tunnel URLs are HTTPS) + echo "Testing with curl..." + if curl -s --connect-timeout 10 "$E2E_ELECTRUM_SERVER" >/dev/null 2>&1; then + echo "โœ… Electrum server is reachable via tunnel" + else + echo "โŒ Electrum server is not reachable via tunnel" + fi + fi + + # Test LND connectivity via tunnel + if [ -n "$E2E_LND_URL" ]; then + echo "Testing LND at $E2E_LND_URL..." + if curl -s --connect-timeout 10 "$E2E_LND_URL" >/dev/null 2>&1; then + echo "โœ… LND is reachable via tunnel" + else + echo "โŒ LND is not reachable via tunnel" + echo "This will cause tests to fail. Check if the regtest infrastructure job is still running." + fi + fi + + # Check if we should continue with tests despite connectivity issues + if [ -n "$E2E_ELECTRUM_SERVER" ] && [ -n "$E2E_LND_URL" ]; then + if ! curl -s --connect-timeout 5 "$E2E_ELECTRUM_SERVER" >/dev/null 2>&1 || ! curl -s --connect-timeout 5 "$E2E_LND_URL" >/dev/null 2>&1; then + echo "โš ๏ธ WARNING: Regtest services are not reachable via tunnel. Tests will likely fail." + echo "This could be due to:" + echo "1. The regtest infrastructure job has stopped" + echo "2. Cloudflare tunnel issues" + echo "3. Network connectivity issues" + echo "" + echo "๐Ÿ”ง SOLUTIONS TO TRY:" + echo "1. Check if the regtest-infrastructure job is still running" + echo "2. Check Cloudflare tunnel logs" + echo "3. Use a different regtest service (e.g., Blockstream's regtest API)" + echo "" + echo "Continuing with tests anyway..." + else + echo "โœ… Regtest services are reachable via Cloudflare tunnels" + fi + fi + + # Add more debugging before running tests + echo "=== Pre-test Debugging ===" + echo "Current directory: $(pwd)" + echo "Simulator status:" + xcrun simctl list devices | grep "$SIMULATOR_NAME" + echo "Appium status:" + curl -s http://localhost:4723/status | head -5 + echo "App status on simulator:" + xcrun simctl listapps "$SIMULATOR_UDID" | grep -i bitkit || echo "App not found" + echo "WebDriverIO config:" + head -20 wdio.conf.ts + echo "Test files:" + find test/specs -name "*.e2e.ts" | head -5 + echo "Package.json scripts:" + grep -A 5 -B 5 "e2e:ios" package.json + echo "=========================" + + # Pass everything through to WDIO/Mocha with more verbose output + echo "Starting WebDriverIO tests..." + echo "Running tests with grep pattern: ${{ matrix.shard.grep }}" + # Try different parameter formats for WebDriverIO/Mocha + if ! npm run e2e:ios -- --grep "${{ matrix.shard.grep }}" 2>&1 | tee "$LOGDIR/webdriverio.log"; then + echo "Grep parameter failed, trying without filter..." + if ! npm run e2e:ios 2>&1 | tee "$LOGDIR/webdriverio.log"; then + echo "WebDriverIO failed with exit code $?" + echo "Checking for error logs..." + if [ -f "$LOGDIR/webdriverio.log" ]; then + echo "WebDriverIO log contents:" + cat "$LOGDIR/webdriverio.log" + fi + exit 1 + fi + fi + env: + RECORD_VIDEO: true + # Use the regtest infrastructure from Ubuntu runner + E2E_ELECTRUM_SERVER: ${{ needs.regtest-infrastructure.outputs.electrum-url }} + E2E_LND_URL: ${{ needs.regtest-infrastructure.outputs.lnd-url }} + + - name: Upload E2E Artifacts (${{ matrix.shard.name }}) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts_${{ matrix.shard.name }}_${{ github.run_number }} + path: bitkit-e2e-tests/artifacts/ + + - name: Shutdown Cloudflare Tunnels + if: always() + run: | + echo "Attempting to shutdown Cloudflare tunnels..." + # Try to kill any cloudflared processes + pkill -f cloudflared || echo "No cloudflared processes found" + # Try to remove PID file if it exists + rm -f cloudflared.pid || echo "No PID file found" + echo "Cloudflare tunnel cleanup completed" diff --git a/.github/workflows/e2e-tests-intel.yml b/.github/workflows/e2e-tests-intel.yml new file mode 100644 index 00000000..cb5b994b --- /dev/null +++ b/.github/workflows/e2e-tests-intel.yml @@ -0,0 +1,85 @@ +name: e2e-tests-intel + +# on: +# workflow_dispatch: +# pull_request: + +jobs: + docker: + runs-on: macos-13 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check system info + run: | + echo "=== System Information ===" + uname -a + sw_vers + echo "=== Available commands ===" + which brew + which docker || echo "Docker not found" + + - name: Install Docker (via Colima) + run: | + brew install colima docker + + # Check available VM types + echo "Available VM types:" + colima start --help | grep -A 10 "vm-type" || echo "Help not available" + + echo "Trying VZ VM type..." + if ! colima start --arch aarch64 --vm-type=vz --memory 4 --cpu 2; then + echo "Direct start failed, trying brew services approach..." + brew services start colima + sleep 10 + fi + + - name: Test Docker + run: | + echo "=== Colima Status ===" + colima status || echo "Colima not running" + + echo "=== Colima Configuration ===" + colima list || echo "No Colima instances" + + echo "=== Colima Logs (startup) ===" + if [ -f "/Users/runner/.colima/_lima/colima/ha.stderr.log" ]; then + echo "Host agent stderr:" + tail -20 "/Users/runner/.colima/_lima/colima/ha.stderr.log" + else + echo "No stderr log found" + fi + + if [ -f "/Users/runner/.colima/_lima/colima/ha.stdout.log" ]; then + echo "Host agent stdout:" + tail -20 "/Users/runner/.colima/_lima/colima/ha.stdout.log" + else + echo "No stdout log found" + fi + + echo "=== System Virtualization Support ===" + sysctl kern.hv_support || echo "No HV support" + sysctl kern.vm_guest || echo "No VM guest info" + + echo "=== Brew Services Status ===" + brew services list | grep colima || echo "No colima service found" + + echo "=== Trying to start Colima manually with VZ ===" + colima start --vm-type=vz --memory 2 --cpu 1 || echo "VZ manual start failed" + + echo "=== Final Colima Status ===" + colima status || echo "Colima still not running" + + echo "=== CONCLUSION ===" + echo "โŒ VZ virtualization is not available on this GitHub Actions runner" + echo "The error shows: 'Virtualization is not available on this hardware'" + echo "This means we cannot run Docker on the same runner as the iOS simulator" + echo "" + echo "๐Ÿ”ง SOLUTIONS:" + echo "1. Use the hybrid approach (Ubuntu for regtest, macOS for iOS tests)" + echo "2. Use a cloud-hosted regtest service" + echo "3. Use a different CI provider that supports virtualization" + echo "" + echo "The hybrid approach in e2e-tests-hybrid.yml is the recommended solution" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..2bf072b8 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,201 @@ +name: e2e-tests + +# on: +# workflow_dispatch: +# inputs: +# e2e_branch: +# description: "Branch of synonymdev/bitkit-e2e-tests to use" +# required: false +# default: "main" +# pull_request: # Disabled - using e2e-tests-hybrid.yml instead + +env: + TERM: xterm-256color + FORCE_COLOR: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + # if: github.event.pull_request.draft == false + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.4' + + - name: Build iOS app + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHATWOOT_API: ${{ secrets.CHATWOOT_API }} + E2E: true + run: | + xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \ + -scheme Bitkit \ + -configuration Debug \ + -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ + -derivedDataPath DerivedData \ + build + + - name: Prepare app for E2E tests + run: | + # Copy the .app bundle to the expected location and name + mkdir -p e2e-app + cp -r DerivedData/Build/Products/Debug-iphonesimulator/Bitkit.app e2e-app/bitkit.app + + - name: Upload iOS app + uses: actions/upload-artifact@v4 + with: + name: bitkit-e2e-ios_${{ github.run_number }} + path: e2e-app/ + + e2e-tests: + # if: github.event.pull_request.draft == false + runs-on: macos-latest + needs: build + + strategy: + fail-fast: false + matrix: + shard: + # - { name: onboarding_backup_numberpad, grep: "@onboarding|@backup|@numberpad" } + # - { name: onchain_boost_receive_widgets, grep: "@onchain|@boost|@receive|@widgets" } + # - { name: settings, grep: "@settings" } + # - { name: security, grep: "@security" } + - { name: onboarding, grep: "@onboarding" } + + name: e2e-tests - ${{ matrix.shard.name }} + + steps: + - name: Show selected E2E branch + env: + E2E_BRANCH: ${{ github.event.inputs.e2e_branch || 'main' }} + run: echo $E2E_BRANCH + + - name: Clone E2E tests + uses: actions/checkout@v4 + with: + repository: synonymdev/bitkit-e2e-tests + path: bitkit-e2e-tests + ref: ${{ github.event.inputs.e2e_branch || 'main' }} + + - name: Download iOS app + uses: actions/download-artifact@v4 + with: + name: bitkit-e2e-ios_${{ github.run_number }} + path: bitkit-e2e-tests/aut + + - name: List iOS app directory contents + run: ls -l bitkit-e2e-tests/aut + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Cache npm cache + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + working-directory: bitkit-e2e-tests + run: npm ci + + - name: Install Docker + run: | + # Install Docker Desktop for Mac + brew install --cask docker + # Start Docker Desktop + open -a Docker + # Wait for Docker to be ready (with longer timeout) + echo "Waiting for Docker to start..." + timeout 120 bash -c 'until docker info >/dev/null 2>&1; do echo "Waiting for Docker..."; sleep 5; done' + echo "Docker is ready!" + + - name: Install Docker Compose + run: | + # Install docker-compose via pip (more reliable than brew on macOS runners) + python3 -m pip install docker-compose + + - name: Docker info + run: | + docker --version + docker info + docker ps + + - name: Run regtest setup + working-directory: bitkit-e2e-tests + run: | + cd docker + mkdir lnd && chmod 777 lnd + docker compose pull --quiet + docker compose up -d + + - name: Wait for electrum server and LND + working-directory: bitkit-e2e-tests + timeout-minutes: 10 + run: | + while ! nc -z '127.0.0.1' 60001; do sleep 1; done + sudo bash -c "while [ ! -f docker/lnd/data/chain/bitcoin/regtest/admin.macaroon ]; do sleep 1; done" + sudo chmod -R 777 docker/lnd + + - name: Setup iOS Simulator + run: | + # Boot iOS Simulator + xcrun simctl boot "iPhone 16" || true + xcrun simctl bootstatus "iPhone 16" -b + + # Install the app + xcrun simctl install "iPhone 16" bitkit-e2e-tests/aut/bitkit.app + + - name: Run E2E Tests (${{ matrix.shard.name }}) + run: | + cd bitkit-e2e-tests + + # Setup logging + LOGDIR="./artifacts" + mkdir -p "$LOGDIR" + LOGFILE="$LOGDIR/simulator.log" + + # Start simulator logging + xcrun simctl spawn "iPhone 16" log stream --predicate 'process == "Bitkit"' --style compact > "$LOGFILE" & + LOG_PID=$! + + # Setup port forwarding for regtest and LND + xcrun simctl spawn "iPhone 16" launchctl load -w /System/Library/LaunchDaemons/com.apple.usbmuxd.plist || true + + # Cleanup function + cleanup() { + kill "$LOG_PID" 2>/dev/null || true + wait "$LOG_PID" 2>/dev/null || true + } + trap cleanup EXIT INT TERM + + # Pass everything through to WDIO/Mocha + npm run e2e:ios -- "$@" + env: + RECORD_VIDEO: true + + - name: Upload E2E Artifacts (${{ matrix.shard.name }}) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts_${{ matrix.shard.name }}_${{ github.run_number }} + path: bitkit-e2e-tests/artifacts/ + + - name: Dump docker logs on failure (${{ matrix.shard.name }}) + if: failure() + uses: jwalton/gh-docker-logs@v2 + \ No newline at end of file diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/unit-tests.yml similarity index 93% rename from .github/workflows/ios-tests.yml rename to .github/workflows/unit-tests.yml index 7bd9a394..8ed82094 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,10 +1,10 @@ -name: iOS Tests +name: unit-tests -on: - push: - branches: [master] - pull_request: - branches: [master] +# on: + # push: + # branches: [master] + # pull_request: + # branches: [master] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml index fdeae66d..45947413 100644 --- a/.github/workflows/validate-translations.yml +++ b/.github/workflows/validate-translations.yml @@ -1,10 +1,10 @@ name: Validate Translations -on: - push: - branches: [master] - pull_request: - branches: [master] +# on: +# push: +# branches: [master] +# pull_request: +# branches: [master] jobs: validate: diff --git a/wdio.conf.ts b/wdio.conf.ts new file mode 100644 index 00000000..69d89862 --- /dev/null +++ b/wdio.conf.ts @@ -0,0 +1,361 @@ +import path from 'node:path'; +import fs from 'node:fs'; + +const isAndroid = process.env.PLATFORM === 'android'; + +export const config: WebdriverIO.Config = { + // + // ==================== + // Runner Configuration + // ==================== + // WebdriverIO supports running e2e tests as well as unit and component tests. + runner: 'local', + tsConfigPath: './tsconfig.json', + + port: 4723, + // + // ================== + // Specify Test Files + // ================== + // Define which test specs should run. The pattern is relative to the directory + // of the configuration file being run. + // + // The specs are defined as an array of spec files (optionally using wildcards + // that will be expanded). The test for each spec file will be run in a separate + // worker process. In order to have a group of spec files run in the same worker + // process simply enclose them in an array within the specs array. + // + // The path of the spec files will be resolved relative from the directory of + // of the config file unless it's absolute. + // + // Spec in [] will be run in a separate worker process. + // Running all specs sequentially for now. + specs: [['./test/specs/**/*.ts']], + // Patterns to exclude. + exclude: [ + // 'path/to/excluded/files' + ], + // + // ============ + // Capabilities + // ============ + // Define your capabilities here. WebdriverIO can run multiple capabilities at the same + // time. Depending on the number of capabilities, WebdriverIO launches several test + // sessions. Within your capabilities you can overwrite the spec and exclude options in + // order to group specific specs to a specific capability. + // + // First, you can define how many instances should be started at the same time. Let's + // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + // files and you set maxInstances to 10, all spec files will get tested at the same time + // and 30 processes will get spawned. The property handles how many capabilities + // from the same test should run tests. + // + maxInstances: 10, + // + // If you have trouble getting all important capabilities together, check out the + // Sauce Labs platform configurator - a great tool to configure your capabilities: + // https://saucelabs.com/platform/platform-configurator + // + capabilities: [ + isAndroid + ? { + platformName: 'Android', + 'appium:automationName': 'UiAutomator2', + 'appium:deviceName': 'Pixel_6', + 'appium:platformVersion': '13.0', + 'appium:app': path.join(__dirname, 'aut', 'bitkit_e2e.apk'), + // 'appium:app': path.join(__dirname, 'aut', 'bitkit_v1.1.2.apk'), + 'appium:autoGrantPermissions': true, + } + : { + platformName: 'iOS', + 'appium:automationName': 'XCUITest', + 'appium:deviceName': 'iPhone 16', + 'appium:platformVersion': '18.5', + 'appium:app': path.join(__dirname, 'aut', 'bitkit.app'), + 'appium:autoAcceptAlerts': true, + 'appium:udid': 'D1CE9A39-FD24-4854-975C-2CA4A6D41565', + 'appium:newCommandTimeout': 300, + 'appium:commandTimeouts': {"implicit": 30000}, + 'appium:launchTimeout': 300000, + 'appium:deviceReadyTimeout': 300000, + 'appium:wdaLaunchTimeout': 300000, + 'appium:wdaConnectionTimeout': 300000, + 'appium:useNewWDA': true, + 'appium:autoLaunch': false, + 'appium:noReset': true, + 'appium:fullReset': false, + }, + ], + + // + // =================== + // Test Configurations + // =================== + // Define all options that are relevant for the WebdriverIO instance here + // + // Level of logging verbosity: trace | debug | info | warn | error | silent + logLevel: 'warn', + // + // Set specific log levels per logger + // loggers: + // - webdriver, webdriverio + // - @wdio/browserstack-service, @wdio/lighthouse-service, @wdio/sauce-service + // - @wdio/mocha-framework, @wdio/jasmine-framework + // - @wdio/local-runner + // - @wdio/sumologic-reporter + // - @wdio/cli, @wdio/config, @wdio/utils + // Level of logging verbosity: trace | debug | info | warn | error | silent + // logLevels: { + // webdriver: 'info', + // '@wdio/appium-service': 'info' + // }, + // + // If you only want to run your tests until a specific amount of tests have failed use + // bail (default is 0 - don't bail, run all tests). + bail: 0, + // + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. + // baseUrl: 'http://localhost:8080', + // + // Default timeout for all waitFor* commands. + waitforTimeout: 30000, + // + // Default timeout in milliseconds for request + // if browser driver or grid doesn't send response + connectionRetryTimeout: 120000, + // + // Default request retries count + connectionRetryCount: 3, + // + // Test runner services + // Services take over a specific job you don't want to take care of. They enhance + // your test setup with almost no effort. Unlike plugins, they don't add new + // commands. Instead, they hook themselves up into the test process. + services: ['appium'], + + // Framework you want to run your specs with. + // The following are supported: Mocha, Jasmine, and Cucumber + // see also: https://webdriver.io/docs/frameworks + // + // Make sure you have the wdio adapter package for the specific framework installed + // before running any tests. + framework: 'mocha', + + // + // The number of times to retry the entire specfile when it fails as a whole + // specFileRetries: 1, + // + // Delay in seconds between the spec file retry attempts + // specFileRetriesDelay: 0, + // + // Whether or not retried spec files should be retried immediately or deferred to the end of the queue + // specFileRetriesDeferred: false, + // + // Test reporter for stdout. + // The only one supported by default is 'dot' + // see also: https://webdriver.io/docs/dot-reporter + reporters: ['spec'], + + // Options to be passed to Mocha. + // See the full list at http://mochajs.org/ + mochaOpts: { + ui: 'bdd', + timeout: 10 * 60 * 1000, // 10 minutes + }, + + // + // ===== + // Hooks + // ===== + // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + // it and to build services around it. You can either apply a single function or an array of + // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + // resolved to continue. + /** + * Gets executed once before all workers get launched. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + */ + // onPrepare: function (config, capabilities) { + // }, + /** + * Gets executed before a worker process is spawned and can be used to initialize specific service + * for that worker as well as modify runtime environments in an async fashion. + * @param {string} cid capability id (e.g 0-0) + * @param {object} caps object containing capabilities for session that will be spawn in the worker + * @param {object} specs specs to be run in the worker process + * @param {object} args object that will be merged with the main configuration once worker is initialized + * @param {object} execArgv list of string arguments passed to the worker process + */ + // onWorkerStart: function (cid, caps, specs, args, execArgv) { + // }, + /** + * Gets executed just after a worker process has exited. + * @param {string} cid capability id (e.g 0-0) + * @param {number} exitCode 0 - success, 1 - fail + * @param {object} specs specs to be run in the worker process + * @param {number} retries number of retries used + */ + // onWorkerEnd: function (cid, exitCode, specs, retries) { + // }, + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {string} cid worker id (e.g. 0-0) + */ + // beforeSession: function (config, capabilities, specs, cid) { + // }, + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {object} browser instance of created browser/device session + */ + // before: function (capabilities, specs) { + // }, + /** + * Runs before a WebdriverIO command gets executed. + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + */ + // beforeCommand: function (commandName, args) { + // }, + /** + * Hook that gets executed before the suite starts + * @param {object} suite suite details + */ + // beforeSuite: function (suite) { + // }, + /** + * Function to be executed before a test (in Mocha/Jasmine) starts. + */ + // beforeTest: function (test, context) { + // }, + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + // beforeHook: function (test, context, hookName) { + // }, + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + // afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) { + // }, + /** + * Function to be executed after a test (in Mocha/Jasmine only) + * @param {object} test test object + * @param {object} context scope object the test was executed with + * @param {Error} result.error error object in case the test fails, otherwise `undefined` + * @param {*} result.result return object of test function + * @param {number} result.duration duration of test + * @param {boolean} result.passed true if test has passed, otherwise false + * @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }` + */ + // afterTest: function(test, context, { error, result, duration, passed, retries }) { + // }, + + /** + * Hook that gets executed after the suite has ended + * @param {object} suite suite details + */ + // afterSuite: function (suite) { + // }, + /** + * Runs after a WebdriverIO command gets executed + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {number} result 0 - command success, 1 - command error + * @param {object} error error object if any + */ + // afterCommand: function (commandName, args, result, error) { + // }, + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {number} result 0 - test pass, 1 - test fail + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // after: function (result, capabilities, specs) { + // }, + /** + * Gets executed right after terminating the webdriver session. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // afterSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed after all workers got shut down and the process is about to exit. An error + * thrown in the onComplete hook will result in the test run failing. + * @param {object} exitCode 0 - success, 1 - fail + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {} results object containing test results + */ + // onComplete: function(exitCode, config, capabilities, results) { + // }, + /** + * Gets executed when a refresh happens. + * @param {string} oldSessionId session ID of the old session + * @param {string} newSessionId session ID of the new session + */ + // onReload: function(oldSessionId, newSessionId) { + // } + /** + * Hook that gets executed before a WebdriverIO assertion happens. + * @param {object} params information about the assertion to be executed + */ + // beforeAssertion: function(params) { + // } + /** + * Hook that gets executed after a WebdriverIO assertion happened. + * @param {object} params information about the assertion that was executed, including its results + */ + // afterAssertion: function(params) { + // } + beforeTest: async function (test) { + if (process.env.RECORD_VIDEO === 'true') { + await driver.startRecordingScreen(); + } + console.log(`๐Ÿงช Start: ${test.parent} - ${test.title}`); + }, + + afterTest: async function (test, _context, { error }) { + if (!error) return; // Skip artifacts if test passed + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const testNameRaw = `${test.parent || 'unknown'}_${test.title}`; + const testName = testNameRaw.replace(/\s+/g, '_').replace(/[^\w-]/g, ''); + const testDir = path.join(__dirname, 'artifacts', testName); + + // Ensure per-test directory exists + fs.mkdirSync(testDir, { recursive: true }); + + // Save screenshot + const screenshotPath = path.join(testDir, `${testName}-${timestamp}.png`); + const screenshot = await driver.takeScreenshot(); + fs.writeFileSync(screenshotPath, screenshot, 'base64'); + console.log(`๐Ÿ“ธ Saved screenshot: ${screenshotPath}`); + + // Save video if recording was enabled + if (process.env.RECORD_VIDEO === 'true') { + const videoBase64 = await driver.stopRecordingScreen(); + const videoPath = path.join(testDir, `${testName}-${timestamp}.mp4`); + fs.writeFileSync(videoPath, videoBase64, 'base64'); + console.log(`๐ŸŽฅ Saved test video: ${videoPath}`); + } + }, +};