diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index 3b49148..5ddc157 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -14,6 +14,7 @@ on: jobs: test-mac: name: Test mac/run.sh on macOS + # if: false runs-on: macos-latest timeout-minutes: 15 environment: BrowserStack @@ -91,15 +92,15 @@ jobs: TURL: https://bstackdemo.com run: | echo "Running integration tests in silent mode..." - + # Set default values if secrets are not provided BROWSERSTACK_USERNAME="${BROWSERSTACK_USERNAME:-test_user}" BROWSERSTACK_ACCESS_KEY="${BROWSERSTACK_ACCESS_KEY:-test_key}" - + export BROWSERSTACK_USERNAME export BROWSERSTACK_ACCESS_KEY export TURL - + # Test configurations test_configs=( "web java" @@ -109,7 +110,7 @@ jobs: "web nodejs" "app nodejs" ) - + for config in "${test_configs[@]}"; do read -r test_type tech_stack <<< "$config" echo "================================" @@ -148,9 +149,9 @@ jobs: fi unset exit_code done - + echo "✅ All integration tests completed" - + - name: Sync BrowserStack logs to workspace if: always() run: | @@ -166,156 +167,198 @@ jobs: with: name: browserstack-logs-macos path: | - ${{ github.workspace }}/bs-logs - /tmp/run_test_*.log + ${{ github.workspace }}/bs-logs + /tmp/run_test_*.log retention-days: 30 if-no-files-found: ignore - # test-windows: - # name: Test win/run.ps1 on Windows - # runs-on: windows-latest - # timeout-minutes: 15 - # environment: BrowserStack - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - # - name: Set up Python 3.12 - # uses: actions/setup-python@v5 - # with: - # python-version: '3.12' - # - name: Check PowerShell version - # run: | - # $PSVersionTable.PSVersion - # Write-Host "✅ PowerShell version check complete" - # - # - name: Validate PowerShell script syntax - # run: | - # Write-Host "Validating win/run.ps1 syntax..." - # $ScriptPath = "win/run.ps1" - # $null = [System.Management.Automation.PSParser]::Tokenize((Get-Content $ScriptPath), [ref]$null) - # Write-Host "✅ win/run.ps1 syntax is valid" - # - # - name: Validate supporting PowerShell scripts syntax - # run: | - # Write-Host "Validating supporting PowerShell scripts..." - # $Scripts = @("win/proxy-check.ps1") - # foreach ($Script in $Scripts) { - # $null = [System.Management.Automation.PSParser]::Tokenize((Get-Content $Script), [ref]$null) - # Write-Host "✅ $Script syntax is valid" - # } - # - # - name: Run PSScriptAnalyzer - # run: | - # Write-Host "Installing PSScriptAnalyzer..." - # Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -ErrorAction SilentlyContinue - # Write-Host "Running PSScriptAnalyzer..." - # Invoke-ScriptAnalyzer -Path "win/run.ps1" -Recurse -ReportSummary || $true - # Write-Host "✅ PSScriptAnalyzer analysis complete" - # - # - name: Check script file encoding - # run: | - # Write-Host "Checking PowerShell script encoding..." - # $ScriptPath = "win/run.ps1" - # $Encoding = (Get-Item $ScriptPath).EncodingInfo - # Write-Host "File encoding: $Encoding" - # Write-Host "✅ Encoding check complete" - # - # - name: Verify required dependencies - # run: | - # Write-Host "Checking required dependencies..." - # if (Get-Command curl.exe -ErrorAction SilentlyContinue) { Write-Host "✅ curl found" } - # if (Get-Command git.exe -ErrorAction SilentlyContinue) { Write-Host "✅ git found" } - # Write-Host "✅ PowerShell dependencies verified" - # - # - name: Integration Test - Silent Mode Execution - # if: success() - # env: - # BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - # BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - # TURL: https://bstackdemo.com - # run: | - # Write-Host "Running integration tests in silent mode..." - # - # # Set default values if secrets are not provided - # $BrowserStackUsername = if ($env:BROWSERSTACK_USERNAME) { $env:BROWSERSTACK_USERNAME } else { "test_user" } - # $BrowserStackAccessKey = if ($env:BROWSERSTACK_ACCESS_KEY) { $env:BROWSERSTACK_ACCESS_KEY } else { "test_key" } - # $TestUrl = $env:TURL - # - # # Export environment variables - # $env:BROWSERSTACK_USERNAME = $BrowserStackUsername - # $env:BROWSERSTACK_ACCESS_KEY = $BrowserStackAccessKey - # $env:TURL = $TestUrl - # - # # Test configurations - # $testConfigs = @( - # @("web", "java"), - # @("app", "java"), - # @("web", "python"), - # @("app", "python"), - # @("web", "nodejs"), - # @("app", "nodejs") - # ) - # - # foreach ($config in $testConfigs) { - # $testType = $config[0] - # $techStack = $config[1] - # - # Write-Host "================================" - # Write-Host "Testing: .\win\run.ps1 --silent $testType $techStack" - # Write-Host "================================" - # - # # Create log file path - # $logPath = "C:\Temp\run_test_${testType}_${techStack}.log" - # New-Item -ItemType Directory -Path "C:\Temp" -Force -ErrorAction SilentlyContinue | Out-Null - # - # # Run with timeout (using job for timeout capability) - # $job = Start-Job -ScriptBlock { - # param($path, $testType, $techStack, $logPath) - # & $path --silent $testType $techStack 2>&1 | Tee-Object -FilePath $logPath -Append - # } -ArgumentList ".\win\run.ps1", $testType, $techStack, $logPath - # - # # Wait for job with 600 second timeout - # $timeout = New-TimeSpan -Seconds 600 - # $completed = Wait-Job -Job $job -Timeout 600 - # - # if ($completed) { - # $result = Receive-Job -Job $job - # if ($job.State -eq "Completed") { - # Write-Host "✅ .\win\run.ps1 --silent $testType $techStack completed successfully" - # } else { - # Write-Host "⚠️ .\win\run.ps1 --silent $testType $techStack exited with state: $($job.State)" - # if (Test-Path $logPath) { - # Write-Host "Log output (last 20 lines):" - # Get-Content -Path $logPath -Tail 20 - # } - # } - # } else { - # Write-Host "⚠️ .\win\run.ps1 --silent $testType $techStack timed out after 600 seconds" - # Stop-Job -Job $job - # if (Test-Path $logPath) { - # Write-Host "Log output (last 20 lines):" - # Get-Content -Path $logPath -Tail 20 - # } - # } - # - # Remove-Job -Job $job -Force - # } - # - # Write-Host "✅ All integration tests completed" - # - # - name: Upload BrowserStack Logs as Artifacts - # if: always() - # uses: actions/upload-artifact@v4 - # with: - # name: browserstack-logs-windows - # path: | - # C:\Users\runneradmin\.browserstack\NOW\logs\ - # C:\Temp\run_test_*.log - # retention-days: 30 - # if-no-files-found: ignore + test-windows: + name: Test win/run.ps1 on Windows + runs-on: windows-latest + timeout-minutes: 25 + environment: BrowserStack + defaults: + run: + shell: powershell + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check PowerShell version + run: | + $PSVersionTable.PSVersion + Write-Host "✅ PowerShell version check complete" + + - name: Validate PowerShell script syntax + run: | + Write-Host "Validating win/run.ps1 syntax..." + $ScriptPath = "win/run.ps1" + $null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $ScriptPath), [ref]$null) + Write-Host "✅ win/run.ps1 syntax is valid" + + - name: Validate supporting PowerShell scripts syntax + run: | + Write-Host "Validating supporting PowerShell scripts..." + $Scripts = @( + "win/common-utils.ps1", + "win/logging-utils.ps1", + "win/env-prequisite-checks.ps1", + "win/user-interaction.ps1", + "win/env-setup-run.ps1", + "win/device-machine-allocation.ps1" + ) + foreach ($Script in $Scripts) { + $null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $Script), [ref]$null) + Write-Host "✅ $Script syntax is valid" + } + + - name: Run PSScriptAnalyzer + run: | + Write-Host "Installing PSScriptAnalyzer if needed..." + if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) { + Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + } + + Write-Host "Running PSScriptAnalyzer..." + Invoke-ScriptAnalyzer -Path "win" -Recurse -ReportSummary -ErrorAction Continue + Write-Host "✅ PSScriptAnalyzer analysis complete (continuing even if issues are found)" + + - name: Check script file encoding + run: | + Write-Host "Checking PowerShell script encoding..." + $ScriptPath = "win/run.ps1" + $bytes = [System.IO.File]::ReadAllBytes($ScriptPath) + + $encoding = "Unknown / ASCII / UTF-8 without BOM" + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + $encoding = "UTF-8 with BOM" + } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + $encoding = "UTF-16 LE" + } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + $encoding = "UTF-16 BE" + } + + Write-Host "Detected encoding (heuristic): $encoding" + Write-Host "✅ Encoding check complete" + + - name: Verify required dependencies + run: | + Write-Host "Checking required dependencies..." + if (Get-Command curl.exe -ErrorAction SilentlyContinue) { Write-Host "✅ curl found" } else { Write-Host "⚠️ curl not found" } + if (Get-Command git.exe -ErrorAction SilentlyContinue) { Write-Host "✅ git found" } else { Write-Host "⚠️ git not found" } + Write-Host "✅ PowerShell dependencies verified" + + - name: Integration Test - Silent Mode Execution + if: success() + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + TURL: https://bstackdemo.com + run: | + Write-Host "Running integration tests in silent mode..." + + # Use defaults if secrets are missing (for local / dry runs) + $BrowserStackUsername = if ($env:BROWSERSTACK_USERNAME) { $env:BROWSERSTACK_USERNAME } else { "test_user" } + $BrowserStackAccessKey = if ($env:BROWSERSTACK_ACCESS_KEY) { $env:BROWSERSTACK_ACCESS_KEY } else { "test_key" } + $TestUrl = $env:TURL + + $env:BROWSERSTACK_USERNAME = $BrowserStackUsername + $env:BROWSERSTACK_ACCESS_KEY = $BrowserStackAccessKey + $env:TURL = $TestUrl + + # Absolute path is safer in CI + $scriptPath = Join-Path $env:GITHUB_WORKSPACE "win\run.ps1" + + $testConfigs = @( + @("web", "java"), + @("app", "java"), + @("web", "python"), + @("app", "python"), + @("web", "nodejs"), + @("app", "nodejs") + ) + + $overallFailed = $false + $logRoot = Join-Path $env:TEMP "now-tests" + New-Item -ItemType Directory -Force -Path $logRoot | Out-Null + + foreach ($config in $testConfigs) { + $testType = $config[0] + $techStack = $config[1] + + Write-Host "================================" + Write-Host "Testing: $scriptPath --silent $testType $techStack" + Write-Host "================================" + + $logPath = Join-Path $logRoot "run_test_${testType}_${techStack}.log" + + & $scriptPath --silent $testType $techStack 2>&1 | Tee-Object -FilePath $logPath -Append + $exitCode = $LASTEXITCODE + + if ($exitCode -eq 0) { + Write-Host "✅ $testType / $techStack completed (exit code: $exitCode)" + } else { + Write-Host "⚠️ $testType / $techStack exited with code: $exitCode" + $overallFailed = $true + + if (Test-Path $logPath) { + Write-Host "Log output (last 20 lines):" + Get-Content -Path $logPath -Tail 20 + } + } + } + + if ($overallFailed) { + Write-Error "One or more configurations failed." + exit 1 + } + + Write-Host "✅ All integration tests completed successfully" + + - name: Sync BrowserStack logs to workspace (Windows) + if: always() + run: | + $dest = Join-Path $env:GITHUB_WORKSPACE "bs-logs" + New-Item -ItemType Directory -Force -Path $dest | Out-Null + + $bsLogPath = Join-Path $env:USERPROFILE ".browserstack\NOW\logs" + $tempLogDir = Join-Path $env:TEMP "now-tests" + + if (Test-Path $bsLogPath) { + Write-Host "Copying logs from $bsLogPath" + Copy-Item -Path (Join-Path $bsLogPath "*") -Destination $dest -Recurse -Force -ErrorAction SilentlyContinue + } else { + Write-Host "No logs found at $bsLogPath" + } + + if (Test-Path $tempLogDir) { + Write-Host "Copying integration logs from $tempLogDir" + Copy-Item -Path (Join-Path $tempLogDir "*") -Destination $dest -Recurse -Force -ErrorAction SilentlyContinue + } + + - name: Upload BrowserStack Logs as Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: browserstack-logs-windows + path: ${{ github.workspace }}/bs-logs + retention-days: 30 + if-no-files-found: ignore test-linux: name: Test mac/run.sh on Linux + # if: false runs-on: ubuntu-latest timeout-minutes: 15 environment: BrowserStack @@ -399,15 +442,15 @@ jobs: TURL: https://bstackdemo.com run: | echo "Running integration tests in silent mode..." - + # Set default values if secrets are not provided BROWSERSTACK_USERNAME="${BROWSERSTACK_USERNAME:-test_user}" BROWSERSTACK_ACCESS_KEY="${BROWSERSTACK_ACCESS_KEY:-test_key}" - + export BROWSERSTACK_USERNAME export BROWSERSTACK_ACCESS_KEY export TURL - + # Test configurations test_configs=( "web java" @@ -417,7 +460,7 @@ jobs: "web nodejs" "app nodejs" ) - + for config in "${test_configs[@]}"; do read -r test_type tech_stack <<< "$config" echo "================================" @@ -440,7 +483,7 @@ jobs: fi unset exit_code done - + echo "✅ All integration tests completed" - name: Sync BrowserStack logs to workspace @@ -467,7 +510,7 @@ jobs: test-summary: name: Test Summary runs-on: ubuntu-latest - needs: [test-mac, test-linux] + needs: [test-mac, test-linux, test-windows] if: always() steps: - name: Check test results @@ -475,8 +518,9 @@ jobs: echo "=== Test Results Summary ===" echo "macOS Tests: ${{ needs.test-mac.result }}" echo "Linux Tests: ${{ needs.test-linux.result }}" - - if [ "${{ needs.test-mac.result }}" = "failure" ] || [ "${{ needs.test-linux.result }}" = "failure" ]; then + echo "Windows Tests: ${{ needs.test-windows.result }}" + + if [ "${{ needs.test-mac.result }}" = "failure" ] || [ "${{ needs.test-linux.result }}" = "failure" ] || [ "${{ needs.test-windows.result }}" = "failure" ]; then echo "❌ Some tests failed" exit 1 fi @@ -487,4 +531,4 @@ jobs: run: | echo "✅ All script validations passed successfully!" echo "- mac/run.sh and supporting scripts validated on macOS and Linux" - echo "- win/run.ps1 and supporting scripts validated on Windows (temporarily disabled)" + echo "- win/run.ps1 and supporting scripts validated on Windows" diff --git a/mac/common-utils.sh b/mac/common-utils.sh index 6a45a57..e41f4f8 100644 --- a/mac/common-utils.sh +++ b/mac/common-utils.sh @@ -88,13 +88,25 @@ handle_app_upload() { log_msg_to "Exported APP_PLATFORM=$APP_PLATFORM" else local choice - choice=$(osascript -e ' - display dialog "How would you like to select your app?" ¬ - with title "BrowserStack App Upload" ¬ - with icon note ¬ - buttons {"Use Sample App", "Upload my App (.apk/.ipa)", "Cancel"} ¬ - default button "Upload my App (.apk/.ipa)" - ' 2>/dev/null) + if [[ "$NOW_OS" == "macos" ]]; then + choice=$(osascript -e ' + display dialog "How would you like to select your app?" ¬ + with title "BrowserStack App Upload" ¬ + with icon note ¬ + buttons {"Use Sample App", "Upload my App (.apk/.ipa)", "Cancel"} ¬ + default button "Upload my App (.apk/.ipa)" + ' 2>/dev/null) + else + echo "How would you like to select your app?" + select opt in "Use Sample App" "Upload my App (.apk/.ipa)" "Cancel"; do + case $opt in + "Use Sample App") choice="Use Sample App"; break ;; + "Upload my App (.apk/.ipa)") choice="Upload my App"; break ;; + "Cancel") choice="Cancel"; break ;; + *) echo "Invalid option";; + esac + done + fi if [[ "$choice" == *"Use Sample App"* ]]; then upload_sample_app @@ -135,9 +147,18 @@ upload_custom_app() { local file_path # Convert to POSIX path - file_path=$(osascript -e \ - 'POSIX path of (choose file with prompt "Select your .apk or .ipa file:" of type {"apk", "ipa"})' \ - 2>/dev/null) + # Convert to POSIX path + if [[ "$NOW_OS" == "macos" ]]; then + file_path=$(osascript -e \ + 'POSIX path of (choose file with prompt "Select your .apk or .ipa file:" of type {"apk", "ipa"})' \ + 2>/dev/null) + else + echo "Please enter the full path to your .apk or .ipa file:" + read -r file_path + # Remove quotes if user added them + file_path="${file_path%\"}" + file_path="${file_path#\"}" + fi # Trim whitespace file_path="${file_path%"${file_path##*[![:space:]]}"}" @@ -210,7 +231,7 @@ fetch_plan_details() { local web_unauthorized=false local mobile_unauthorized=false - if [[ "$test_type" == "web" || "$test_type" == "both" ]]; then + if [[ "$test_type" == "web" ]]; then RESPONSE_WEB=$(curl -s -w "\n%{http_code}" -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" https://api.browserstack.com/automate/plan.json) HTTP_CODE_WEB=$(echo "$RESPONSE_WEB" | tail -n1) RESPONSE_WEB_BODY=$(echo "$RESPONSE_WEB" | sed '$d') @@ -225,7 +246,7 @@ fetch_plan_details() { fi fi - if [[ "$test_type" == "app" || "$test_type" == "both" ]]; then + if [[ "$test_type" == "app" ]]; then RESPONSE_MOBILE=$(curl -s -w "\n%{http_code}" -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" https://api-cloud.browserstack.com/app-automate/plan.json) HTTP_CODE_MOBILE=$(echo "$RESPONSE_MOBILE" | tail -n1) RESPONSE_MOBILE_BODY=$(echo "$RESPONSE_MOBILE" | sed '$d') @@ -243,11 +264,21 @@ fetch_plan_details() { log_info "Plan summary: Web $WEB_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_WEB max), Mobile $MOBILE_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_MOBILE max)" if [[ "$test_type" == "web" && "$web_unauthorized" == true ]] || \ - [[ "$test_type" == "app" && "$mobile_unauthorized" == true ]] || \ - [[ "$test_type" == "both" && "$web_unauthorized" == true && "$mobile_unauthorized" == true ]]; then + [[ "$test_type" == "app" && "$mobile_unauthorized" == true ]]; then log_msg_to "❌ Unauthorized to fetch required plan(s). Exiting." exit 1 fi + + if [[ "$RUN_MODE" == *"--silent"* ]]; then + if [[ "$test_type" == "web" ]]; then + TEAM_PARALLELS_MAX_ALLOWED_WEB=5 + export TEAM_PARALLELS_MAX_ALLOWED_WEB=5 + else + TEAM_PARALLELS_MAX_ALLOWED_MOBILE=5 + export TEAM_PARALLELS_MAX_ALLOWED_MOBILE=5 + fi + log_info "Resetting Plan summary: Web $WEB_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_WEB max), Mobile $MOBILE_PLAN_FETCHED ($TEAM_PARALLELS_MAX_ALLOWED_MOBILE max)" + fi } # Function to check if IP is private @@ -270,7 +301,7 @@ is_domain_private() { export CX_TEST_URL="$CX_TEST_URL" # Resolve domain using Cloudflare DNS - IP_ADDRESS=$(dig +short "$domain" @1.1.1.1 | head -n1) + IP_ADDRESS=$(resolve_ip "$domain") # Determine if domain is private if is_private_ip "$IP_ADDRESS"; then @@ -284,6 +315,31 @@ is_domain_private() { return $is_cx_domain_private } +resolve_ip() { + local domain=$1 + local ip="" + + # Try dig first (standard on macOS/Linux, optional on Windows) + if command -v dig >/dev/null 2>&1; then + ip=$(dig +short "$domain" @1.1.1.1 | head -n1) + fi + + # Try Python if dig failed or missing + if [ -z "$ip" ] && command -v python3 >/dev/null 2>&1; then + ip=$(python3 -c "import socket; print(socket.gethostbyname('$domain'))" 2>/dev/null) + fi + + # Try nslookup as last resort (parsing is fragile) + if [ -z "$ip" ] && command -v nslookup >/dev/null 2>&1; then + # Windows/Generic nslookup parsing + # Look for "Address:" or "Addresses:" after "Name:" + # This is a best-effort attempt + ip=$(nslookup "$domain" 2>/dev/null | grep -A 10 "Name:" | grep "Address" | tail -n1 | awk '{print $NF}') + fi + + echo "$ip" +} + identify_run_status_java() { local log_file=$1 @@ -401,4 +457,3 @@ detect_os() { export NOW_OS=$response } - diff --git a/mac/env-setup-run.sh b/mac/env-setup-run.sh index 5cb1327..398ce24 100644 --- a/mac/env-setup-run.sh +++ b/mac/env-setup-run.sh @@ -23,7 +23,7 @@ setup_environment() { # Calculate parallels local total_parallels - total_parallels=$(echo "$max_parallels" | bc | cut -d'.' -f1) + total_parallels=$(awk -v n="$max_parallels" 'BEGIN { printf "%d", n }') [ -z "$total_parallels" ] && total_parallels=1 local parallels_per_platform=$total_parallels @@ -536,7 +536,13 @@ detect_setup_python_env() { exit 1 } - # shellcheck source=/dev/null - source .venv/bin/activate + # Activate virtual environment (handle Windows/Unix paths) + if [ -f ".venv/Scripts/activate" ]; then + # shellcheck source=/dev/null + source .venv/Scripts/activate + else + # shellcheck source=/dev/null + source .venv/bin/activate + fi log_success "Virtual environment created and activated." -} +} \ No newline at end of file diff --git a/mac/user-interaction.sh b/mac/user-interaction.sh index 752abfe..d671e50 100644 --- a/mac/user-interaction.sh +++ b/mac/user-interaction.sh @@ -10,16 +10,29 @@ get_browserstack_credentials() { access_key="$BROWSERSTACK_ACCESS_KEY" log_info "BrowserStack credentials loaded from environment variables for user: $username" else + if [[ "$NOW_OS" == "macos" ]]; then username=$(osascript -e 'Tell application "System Events" to display dialog "Please enter your BrowserStack Username.\n\nNote: Locate it in your BrowserStack account profile page.\nhttps://www.browserstack.com/accounts/profile/details" default answer "" with title "BrowserStack Setup" buttons {"OK"} default button "OK"' \ -e 'text returned of result') + else + echo "Please enter your BrowserStack Username." + echo "Note: Locate it in your BrowserStack account profile page: https://www.browserstack.com/accounts/profile/details" + read -r username + fi if [ -z "$username" ]; then log_msg_to "❌ Username empty" return 1 fi + if [[ "$NOW_OS" == "macos" ]]; then access_key=$(osascript -e 'Tell application "System Events" to display dialog "Please enter your BrowserStack Access Key.\n\nNote: Locate it in your BrowserStack account page.\nhttps://www.browserstack.com/accounts/profile/details" default answer "" with hidden answer with title "BrowserStack Setup" buttons {"OK"} default button "OK"' \ -e 'text returned of result') + else + echo "Please enter your BrowserStack Access Key." + echo "Note: Locate it in your BrowserStack account page: https://www.browserstack.com/accounts/profile/details" + read -rs access_key + echo "" # Newline after secret input + fi if [ -z "$access_key" ]; then log_msg_to "❌ Access Key empty" return 1 @@ -41,8 +54,18 @@ get_tech_stack() { tech_stack="$TSTACK" log_msg_to "✅ Selected Tech Stack from environment: $tech_stack" else + if [[ "$NOW_OS" == "macos" ]]; then tech_stack=$(osascript -e 'Tell application "System Events" to display dialog "Select installed tech stack:" buttons {"java", "python", "nodejs"} default button "java" with title "Testing Framework Technology Stack"' \ -e 'button returned of result') + else + echo "Select installed tech stack:" + select opt in "java" "python" "nodejs"; do + case $opt in + "java"|"python"|"nodejs") tech_stack=$opt; break ;; + *) echo "Invalid option";; + esac + done + fi fi log_msg_to "✅ Selected Tech Stack: $tech_stack" log_info "Tech Stack: $tech_stack" @@ -56,8 +79,14 @@ get_tech_stack() { get_test_url() { local test_url=$DEFAULT_TEST_URL - test_url=$(osascript -e 'Tell application "System Events" to display dialog "Enter the URL you want to test with BrowserStack:\n(Leave blank for default: '"$DEFAULT_TEST_URL"')" default answer "" with title "Test URL Setup" buttons {"OK"} default button "OK"' \ - -e 'text returned of result') + if [[ "$NOW_OS" == "macos" ]]; then + test_url=$(osascript -e 'Tell application "System Events" to display dialog "Enter the URL you want to test with BrowserStack:\n(Leave blank for default: '"$DEFAULT_TEST_URL"')" default answer "" with title "Test URL Setup" buttons {"OK"} default button "OK"' \ + -e 'text returned of result') + else + echo "Enter the URL you want to test with BrowserStack:" + echo "(Leave blank for default: $DEFAULT_TEST_URL)" + read -r test_url + fi if [ -n "$test_url" ]; then log_msg_to "🌐 Using custom test URL: $test_url" @@ -79,8 +108,18 @@ get_test_type() { test_type=$TT log_msg_to "✅ Selected Testing Type from environment: $TEST_TYPE" else + if [[ "$NOW_OS" == "macos" ]]; then test_type=$(osascript -e 'Tell application "System Events" to display dialog "Select testing type:" buttons {"web", "app"} default button "web" with title "Testing Type"' \ -e 'button returned of result') + else + echo "Select testing type:" + select opt in "web" "app"; do + case $opt in + "web"|"app") test_type=$opt; break ;; + *) echo "Invalid option";; + esac + done + fi log_msg_to "✅ Selected Testing Type: $TEST_TYPE" RUN_MODE=$test_type log_info "Run Mode: ${RUN_MODE:-default}" @@ -98,10 +137,6 @@ perform_next_steps_based_on_test_type() { "app") get_upload_app ;; - "both") - get_test_url - get_upload_app - ;; esac } diff --git a/win/common-utils.ps1 b/win/common-utils.ps1 new file mode 100644 index 0000000..2a97751 --- /dev/null +++ b/win/common-utils.ps1 @@ -0,0 +1,360 @@ +# Common helpers shared by the Windows BrowserStack NOW scripts. + +# ===== Global Variables ===== +$script:WORKSPACE_DIR = Join-Path $env:USERPROFILE ".browserstack" +$script:PROJECT_FOLDER = "NOW" +$script:NOW_OS = "windows" + + +$script:GLOBAL_DIR = Join-Path $WORKSPACE_DIR $PROJECT_FOLDER +$script:LOG_DIR = Join-Path $GLOBAL_DIR "logs" +$script:GLOBAL_LOG = "" +$script:WEB_LOG = "" +$script:MOBILE_LOG = "" + +# Script state +$script:BROWSERSTACK_USERNAME = "" +$script:BROWSERSTACK_ACCESS_KEY = "" +$script:TEST_TYPE = "" # Web / App +$script:TECH_STACK = "" # Java / Python / JS +[double]$script:PARALLEL_PERCENTAGE = 1.00 + +$script:WEB_PLAN_FETCHED = $false +$script:MOBILE_PLAN_FETCHED = $false +[int]$script:TEAM_PARALLELS_MAX_ALLOWED_WEB = 0 +[int]$script:TEAM_PARALLELS_MAX_ALLOWED_MOBILE = 0 + +# URL handling +$script:DEFAULT_TEST_URL = "https://bstackdemo.com" +$script:CX_TEST_URL = $DEFAULT_TEST_URL + +# App handling +$script:APP_URL = "" +$script:APP_PLATFORM = "" # ios | android | all + +# Chosen Python command tokens (set during validation when Python is selected) +$script:PY_CMD = @() + +# ===== Error patterns ===== +$script:WEB_SETUP_ERRORS = @("") +$script:WEB_LOCAL_ERRORS = @("") +$script:MOBILE_SETUP_ERRORS= @("") +$script:MOBILE_LOCAL_ERRORS= @("") + +# ===== Workspace Management ===== +function Ensure-Workspace { + if (!(Test-Path $GLOBAL_DIR)) { + New-Item -ItemType Directory -Path $GLOBAL_DIR | Out-Null + Log-Line "✅ Created Onboarding workspace: $GLOBAL_DIR" $GLOBAL_LOG + } else { + Log-Line "✅ Onboarding workspace found at: $GLOBAL_DIR" $GLOBAL_LOG + } +} + +function Setup-Workspace { + Log-Section "⚙️ Environment & Credentials" $GLOBAL_LOG + Ensure-Workspace +} + +function Clear-OldLogs { + if (!(Test-Path $LOG_DIR)) { + New-Item -ItemType Directory -Path $LOG_DIR | Out-Null + } + + $legacyLogs = @("global.log","web_run_result.log","mobile_run_result.log") + foreach ($legacy in $legacyLogs) { + $legacyPath = Join-Path $LOG_DIR $legacy + if (Test-Path $legacyPath) { + Remove-Item -Path $legacyPath -Force -ErrorAction SilentlyContinue + } + } + + Log-Line "✅ Logs directory cleaned. Legacy files removed." $GLOBAL_LOG +} + +# ===== Git Clone ===== +function Invoke-GitClone { + param( + [Parameter(Mandatory)] [string]$Url, + [Parameter(Mandatory)] [string]$Target, + [string]$Branch, + [string]$LogFile + ) + $args = @("clone") + if ($Branch) { $args += @("-b", $Branch) } + $args += @($Url, $Target) + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "git" + $psi.Arguments = ($args | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.WorkingDirectory = (Get-Location).Path + + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $psi + [void]$p.Start() + $stdout = $p.StandardOutput.ReadToEnd() + $stderr = $p.StandardError.ReadToEnd() + $p.WaitForExit() + + if ($LogFile) { + if ($stdout) { Add-Content -Path $LogFile -Value $stdout } + if ($stderr) { Add-Content -Path $LogFile -Value $stderr } + } + + if ($p.ExitCode -ne 0) { + throw "git clone failed (exit $($p.ExitCode)): $stderr" + } +} + +function Set-ContentNoBom { + param( + [Parameter(Mandatory)][string]$Path, + [Parameter(Mandatory)][string]$Value + ) + $enc = New-Object System.Text.UTF8Encoding($false) # no BOM + [System.IO.File]::WriteAllText($Path, $Value, $enc) +} + +function Invoke-External { + param( + [Parameter(Mandatory)][string]$Exe, + [Parameter()][string[]]$Arguments = @(), + [string]$LogFile, + [string]$WorkingDirectory + ) + $psi = New-Object System.Diagnostics.ProcessStartInfo + $exeToRun = $Exe + $argLine = ($Arguments | ForEach-Object { if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } }) -join ' ' + + $ext = [System.IO.Path]::GetExtension($Exe) + if ($ext -and ($ext.ToLowerInvariant() -in @('.cmd','.bat'))) { + if (-not (Test-Path $Exe)) { throw "Command not found: $Exe" } + $psi.FileName = "cmd.exe" + $psi.Arguments = "/c `"$Exe`" $argLine" + } else { + $psi.FileName = $exeToRun + $psi.Arguments = $argLine + } + + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) { + $psi.WorkingDirectory = (Get-Location).Path + } else { + $psi.WorkingDirectory = $WorkingDirectory + } + + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $psi + + if ($LogFile) { + $logDir = Split-Path -Parent $LogFile + if ($logDir -and !(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null } + + $stdoutAction = { + if (-not [string]::IsNullOrEmpty($EventArgs.Data)) { + Add-Content -Path $Event.MessageData -Value $EventArgs.Data + } + } + $stderrAction = { + if (-not [string]::IsNullOrEmpty($EventArgs.Data)) { + Add-Content -Path $Event.MessageData -Value $EventArgs.Data + } + } + + $stdoutEvent = Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $stdoutAction -MessageData $LogFile + $stderrEvent = Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $stderrAction -MessageData $LogFile + + [void]$p.Start() + $p.BeginOutputReadLine() + $p.BeginErrorReadLine() + $p.WaitForExit() + + Unregister-Event -SourceIdentifier $stdoutEvent.Name + Unregister-Event -SourceIdentifier $stderrEvent.Name + Remove-Job -Id $stdoutEvent.Id -Force + Remove-Job -Id $stderrEvent.Id -Force + } else { + [void]$p.Start() + $stdout = $p.StandardOutput.ReadToEnd() + $stderr = $p.StandardError.ReadToEnd() + $p.WaitForExit() + } + + return $p.ExitCode +} + +function Get-MavenCommand { + param([Parameter(Mandatory)][string]$RepoDir) + $mvnCmd = Get-Command mvn -ErrorAction SilentlyContinue + if ($mvnCmd) { return $mvnCmd.Source } + $wrapper = Join-Path $RepoDir "mvnw.cmd" + if (Test-Path $wrapper) { return $wrapper } + throw "Maven not found in PATH and 'mvnw.cmd' not present under $RepoDir. Install Maven or ensure the wrapper exists." +} + +function Get-VenvPython { + param([Parameter(Mandatory)][string]$VenvDir) + $py = Join-Path $VenvDir "Scripts\python.exe" + if (Test-Path $py) { return $py } + throw "Python interpreter not found in venv: $VenvDir" +} + +function Set-PythonCmd { + $candidates = @( + @("python3"), + @("python"), + @("py","-3"), + @("py") + ) + foreach ($cand in $candidates) { + try { + $exe = $cand[0] + $args = @() + if ($cand.Length -gt 1) { $args = $cand[1..($cand.Length-1)] } + $code = Invoke-External -Exe $exe -Arguments ($args + @("--version")) -LogFile $null + if ($code -eq 0) { + $script:PY_CMD = $cand + return + } + } catch {} + } + throw "Python not found via python3/python/py. Please install Python 3 and ensure it's on PATH." +} + +function Invoke-Py { + param( + [Parameter(Mandatory)][string[]]$Arguments, + [string]$LogFile, + [string]$WorkingDirectory + ) + if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } + $exe = $PY_CMD[0] + $baseArgs = @() + if ($PY_CMD.Count -gt 1) { $baseArgs = $PY_CMD[1..($PY_CMD.Count-1)] } + return (Invoke-External -Exe $exe -Arguments ($baseArgs + $Arguments) -LogFile $LogFile -WorkingDirectory $WorkingDirectory) +} + +function Show-Spinner { + param([Parameter(Mandatory)][System.Diagnostics.Process]$Process) + $spin = @('|','/','-','\') + $i = 0 + $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + while (!$Process.HasExited) { + Write-Host "`r[$ts] ⏳ Processing... $($spin[$i])" -NoNewline + $i = ($i + 1) % 4 + Start-Sleep -Milliseconds 100 + } + Write-Host "`r[$ts] ✅ Done! " +} + +function Test-PrivateIP { + param([string]$IP) + if ([string]::IsNullOrWhiteSpace($IP)) { return $false } + $parts = $IP.Split('.') + if ($parts.Count -ne 4) { return $false } + $first = [int]$parts[0] + $second = [int]$parts[1] + if ($first -eq 10) { return $true } + if ($first -eq 192 -and $second -eq 168) { return $true } + if ($first -eq 172 -and $second -ge 16 -and $second -le 31) { return $true } + return $false +} + +function Test-DomainPrivate { + $domain = $CX_TEST_URL -replace '^https?://', '' -replace '/.*$', '' + Log-Line "Website domain: $domain" $GLOBAL_LOG + $env:NOW_WEB_DOMAIN = $CX_TEST_URL + + $IP_ADDRESS = "" + try { + $dnsResult = Resolve-DnsName -Name $domain -Type A -ErrorAction Stop | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1 + if ($dnsResult) { + $IP_ADDRESS = $dnsResult.IPAddress + } + } catch { + try { + $nslookupOutput = nslookup $domain 2>&1 | Out-String + if ($nslookupOutput -match '(?:Address|Addresses):\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})') { + $IP_ADDRESS = $matches[1] + } + } catch { + Log-Line "⚠️ Failed to resolve domain: $domain (assuming public domain)" $GLOBAL_LOG + $IP_ADDRESS = "" + } + } + + if ([string]::IsNullOrWhiteSpace($IP_ADDRESS)) { + Log-Line "⚠️ DNS resolution failed for: $domain (treating as public domain, BrowserStack Local will be DISABLED)" $GLOBAL_LOG + } else { + Log-Line "✅ Resolved IP: $IP_ADDRESS" $GLOBAL_LOG + } + + return (Test-PrivateIP -IP $IP_ADDRESS) +} + +function Get-BasicAuthHeader { + param([string]$User, [string]$Key) + $pair = "{0}:{1}" -f $User,$Key + $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair) + "Basic {0}" -f [System.Convert]::ToBase64String($bytes) +} + +# ===== Fetch plan details ===== +function Fetch-Plan-Details { + param([string]$TestType) + + if ([string]::IsNullOrWhiteSpace($TestType)) { + throw "Test type is required to fetch plan details." + } + + $normalized = $TestType.ToLowerInvariant() + Log-Line "ℹ️ Fetching BrowserStack plan for $normalized" $GLOBAL_LOG + + $auth = Get-BasicAuthHeader -User $BROWSERSTACK_USERNAME -Key $BROWSERSTACK_ACCESS_KEY + $headers = @{ Authorization = $auth } + + switch ($normalized) { + "web" { + try { + $resp = Invoke-RestMethod -Method Get -Uri "https://api.browserstack.com/automate/plan.json" -Headers $headers + $script:WEB_PLAN_FETCHED = $true + $script:TEAM_PARALLELS_MAX_ALLOWED_WEB = [int]$resp.parallel_sessions_max_allowed + Log-Line "✅ Web Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_WEB" $GLOBAL_LOG + } catch { + Log-Line "❌ Web Testing Plan fetch failed ($($_.Exception.Message))" $GLOBAL_LOG + } + if (-not $WEB_PLAN_FETCHED) { + throw "Unable to fetch Web Testing plan details." + } + } + "app" { + try { + $resp2 = Invoke-RestMethod -Method Get -Uri "https://api-cloud.browserstack.com/app-automate/plan.json" -Headers $headers + $script:MOBILE_PLAN_FETCHED = $true + $script:TEAM_PARALLELS_MAX_ALLOWED_MOBILE = [int]$resp2.parallel_sessions_max_allowed + Log-Line "✅ Mobile App Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_MOBILE" $GLOBAL_LOG + } catch { + Log-Line "❌ Mobile App Testing Plan fetch failed ($($_.Exception.Message))" $GLOBAL_LOG + } + if (-not $MOBILE_PLAN_FETCHED) { + throw "Unable to fetch Mobile App Testing plan details." + } + } + default { + throw "Unsupported TEST_TYPE: $TestType. Allowed values: Web, App." + } + } + + Log-Line "ℹ️ Plan summary: Web fetched=$WEB_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_WEB), Mobile fetched=$MOBILE_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_MOBILE)" $GLOBAL_LOG +} + + diff --git a/win/device-machine-allocation.ps1 b/win/device-machine-allocation.ps1 new file mode 100644 index 0000000..35c7a08 --- /dev/null +++ b/win/device-machine-allocation.ps1 @@ -0,0 +1,285 @@ +# Device and platform allocation utilities for the Windows BrowserStack NOW flow. +# Mirrors the macOS shell script structure so we can share logic between both platforms. + +# ===== Example Platform Templates ===== +$WEB_PLATFORM_TEMPLATES = @( + "Windows|10|Chrome", + "Windows|10|Firefox", + "Windows|11|Edge", + "Windows|11|Chrome", + "Windows|8|Chrome", + "OS X|Monterey|Chrome", + "OS X|Ventura|Chrome", + "OS X|Catalina|Firefox" +) + +# Mobile tiers (kept for parity) +$MOBILE_TIER1 = @( + "ios|iPhone 15|17", + "ios|iPhone 15 Pro|17", + "ios|iPhone 16|18", + "android|Samsung Galaxy S25|15", + "android|Samsung Galaxy S24|14" +) +$MOBILE_TIER2 = @( + "ios|iPhone 14 Pro|16", + "ios|iPhone 14|16", + "ios|iPad Air 13 2025|18", + "android|Samsung Galaxy S23|13", + "android|Samsung Galaxy S22|12", + "android|Samsung Galaxy S21|11", + "android|Samsung Galaxy Tab S10 Plus|15" +) +$MOBILE_TIER3 = @( + "ios|iPhone 13 Pro Max|15", + "ios|iPhone 13|15", + "ios|iPhone 12 Pro|14", + "ios|iPhone 12 Pro|17", + "ios|iPhone 12|17", + "ios|iPhone 12|14", + "ios|iPhone 12 Pro Max|16", + "ios|iPhone 13 Pro|15", + "ios|iPhone 13 Mini|15", + "ios|iPhone 16 Pro|18", + "ios|iPad 9th|15", + "ios|iPad Pro 12.9 2020|14", + "ios|iPad Pro 12.9 2020|16", + "ios|iPad 8th|16", + "android|Samsung Galaxy S22 Ultra|12", + "android|Samsung Galaxy S21|12", + "android|Samsung Galaxy S21 Ultra|11", + "android|Samsung Galaxy S20|10", + "android|Samsung Galaxy M32|11", + "android|Samsung Galaxy Note 20|10", + "android|Samsung Galaxy S10|9", + "android|Samsung Galaxy Note 9|8", + "android|Samsung Galaxy Tab S8|12", + "android|Google Pixel 9|15", + "android|Google Pixel 6 Pro|13", + "android|Google Pixel 8|14", + "android|Google Pixel 7|13", + "android|Google Pixel 6|12", + "android|Vivo Y21|11", + "android|Vivo Y50|10", + "android|Oppo Reno 6|11" +) +$MOBILE_TIER4 = @( + "ios|iPhone 15 Pro Max|17", + "ios|iPhone 15 Pro Max|26", + "ios|iPhone 15|26", + "ios|iPhone 15 Plus|17", + "ios|iPhone 14 Pro|26", + "ios|iPhone 14|18", + "ios|iPhone 14|26", + "ios|iPhone 13 Pro Max|18", + "ios|iPhone 13|16", + "ios|iPhone 13|17", + "ios|iPhone 13|18", + "ios|iPhone 12 Pro|18", + "ios|iPhone 14 Pro Max|16", + "ios|iPhone 14 Plus|16", + "ios|iPhone 11|13", + "ios|iPhone 8|11", + "ios|iPhone 7|10", + "ios|iPhone 17 Pro Max|26", + "ios|iPhone 17 Pro|26", + "ios|iPhone 17 Air|26", + "ios|iPhone 17|26", + "ios|iPhone 16e|18", + "ios|iPhone 16 Pro Max|18", + "ios|iPhone 16 Plus|18", + "ios|iPhone SE 2020|16", + "ios|iPhone SE 2022|15", + "ios|iPad Air 4|14", + "ios|iPad 9th|18", + "ios|iPad Air 5|26", + "ios|iPad Pro 11 2021|18", + "ios|iPad Pro 13 2024|17", + "ios|iPad Pro 12.9 2021|14", + "ios|iPad Pro 12.9 2021|17", + "ios|iPad Pro 11 2024|17", + "ios|iPad Air 6|17", + "ios|iPad Pro 12.9 2022|16", + "ios|iPad Pro 11 2022|16", + "ios|iPad 10th|16", + "ios|iPad Air 13 2025|26", + "ios|iPad Pro 11 2020|13", + "ios|iPad Pro 11 2020|16", + "ios|iPad 8th|14", + "ios|iPad Mini 2021|15", + "ios|iPad Pro 12.9 2018|12", + "ios|iPad 6th|11", + "android|Samsung Galaxy S23 Ultra|13", + "android|Samsung Galaxy S22 Plus|12", + "android|Samsung Galaxy S21 Plus|11", + "android|Samsung Galaxy S20 Ultra|10", + "android|Samsung Galaxy S25 Ultra|15", + "android|Samsung Galaxy S24 Ultra|14", + "android|Samsung Galaxy M52|11", + "android|Samsung Galaxy A52|11", + "android|Samsung Galaxy A51|10", + "android|Samsung Galaxy A11|10", + "android|Samsung Galaxy A10|9", + "android|Samsung Galaxy Tab A9 Plus|14", + "android|Samsung Galaxy Tab S9|13", + "android|Samsung Galaxy Tab S7|10", + "android|Samsung Galaxy Tab S7|11", + "android|Samsung Galaxy Tab S6|9", + "android|Google Pixel 9|16", + "android|Google Pixel 10 Pro XL|16", + "android|Google Pixel 10 Pro|16", + "android|Google Pixel 10|16", + "android|Google Pixel 9 Pro XL|15", + "android|Google Pixel 9 Pro|15", + "android|Google Pixel 6 Pro|12", + "android|Google Pixel 6 Pro|15", + "android|Google Pixel 8 Pro|14", + "android|Google Pixel 7 Pro|13", + "android|Google Pixel 5|11", + "android|OnePlus 13R|15", + "android|OnePlus 12R|14", + "android|OnePlus 11R|13", + "android|OnePlus 9|11", + "android|OnePlus 8|10", + "android|Motorola Moto G71 5G|11", + "android|Motorola Moto G9 Play|10", + "android|Vivo V21|11", + "android|Oppo A96|11", + "android|Oppo Reno 3 Pro|10", + "android|Xiaomi Redmi Note 11|11", + "android|Xiaomi Redmi Note 9|10", + "android|Huawei P30|9" +) + +# MOBILE_ALL combines the tiers +$MOBILE_ALL = @() +$MOBILE_ALL += $MOBILE_TIER1 +$MOBILE_ALL += $MOBILE_TIER2 +$MOBILE_ALL += $MOBILE_TIER3 +$MOBILE_ALL += $MOBILE_TIER4 + +# ===== Generators ===== +function Generate-Web-Platforms-Yaml { + param([int]$MaxTotalParallels) + $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) + if ($max -lt 0) { $max = 0 } + $sb = New-Object System.Text.StringBuilder + $count = 0 + + foreach ($t in $WEB_PLATFORM_TEMPLATES) { + $parts = $t.Split('|') + $os = $parts[0]; $osVersion = $parts[1]; $browserName = $parts[2] + foreach ($version in @('latest','latest-1','latest-2')) { + [void]$sb.AppendLine(" - os: $os") + [void]$sb.AppendLine(" osVersion: $osVersion") + [void]$sb.AppendLine(" browserName: $browserName") + [void]$sb.AppendLine(" browserVersion: $version") + $count++ + if ($count -ge $max -and $max -gt 0) { + return $sb.ToString() + } + } + } + return $sb.ToString() +} + +function Generate-Mobile-Platforms-Yaml { + param([int]$MaxTotalParallels) + $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) + if ($max -lt 1) { $max = 1 } + $sb = New-Object System.Text.StringBuilder + $count = 0 + + foreach ($t in $MOBILE_ALL) { + $parts = $t.Split('|') + $platformName = $parts[0] + $deviceName = $parts[1] + $platformVer = $parts[2] + + if (-not [string]::IsNullOrWhiteSpace($APP_PLATFORM)) { + if ($APP_PLATFORM -eq 'ios' -and $platformName -ne 'ios') { continue } + if ($APP_PLATFORM -eq 'android' -and $platformName -ne 'android') { continue } + } + + [void]$sb.AppendLine(" - platformName: $platformName") + [void]$sb.AppendLine(" deviceName: $deviceName") + [void]$sb.AppendLine(" platformVersion: '${platformVer}.0'") + $count++ + if ($count -ge $max) { return $sb.ToString() } + } + return $sb.ToString() +} + +function Generate-Mobile-Caps-Json { + param([int]$MaxTotalParallels, [string]$OutputFile) + $json = Generate-Mobile-Caps-Json-String -MaxTotalParallels $MaxTotalParallels + Set-ContentNoBom -Path $OutputFile -Value $json + return $json +} + +function Generate-Mobile-Caps-Json-String { + param([int]$MaxTotalParallels) + $max = $MaxTotalParallels + if ($max -lt 1) { $max = 1 } + + $items = @() + $count = 0 + + foreach ($t in $MOBILE_ALL) { + $parts = $t.Split('|') + $platformName = $parts[0] + $deviceName = $parts[1] + $platformVer = $parts[2] + + # Filter based on APP_PLATFORM + if (-not [string]::IsNullOrWhiteSpace($APP_PLATFORM)) { + if ($APP_PLATFORM -eq 'ios' -and $platformName -ne 'ios') { continue } + if ($APP_PLATFORM -eq 'android' -and $platformName -ne 'android') { continue } + } + + $items += [pscustomobject]@{ + 'bstack:options' = @{ + deviceName = $deviceName + osVersion = "${platformVer}.0" + } + } + $count++ + if ($count -ge $max) { break } + } + + $json = ($items | ConvertTo-Json -Depth 5 -Compress) + return $json +} + +function Generate-Web-Caps-Json { + param([int]$MaxTotalParallels) + $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) + if ($max -lt 1) { $max = 1 } + + $items = @() + $count = 0 + foreach ($t in $WEB_PLATFORM_TEMPLATES) { + $parts = $t.Split('|') + $os = $parts[0]; $osVersion = $parts[1]; $browserName = $parts[2] + foreach ($version in @('latest','latest-1','latest-2')) { + $items += [pscustomobject]@{ + browserName = $browserName + browserVersion = $version + 'bstack:options' = @{ + os = $os + osVersion = $osVersion + } + } + $count++ + if ($count -ge $max) { break } + } + if ($count -ge $max) { break } + } + + # Return valid JSON array (keep the brackets!) + $json = ($items | ConvertTo-Json -Depth 5 -Compress) + return $json +} + + + diff --git a/win/env-prequisite-checks.ps1 b/win/env-prequisite-checks.ps1 new file mode 100644 index 0000000..e384d55 --- /dev/null +++ b/win/env-prequisite-checks.ps1 @@ -0,0 +1,148 @@ +# Environment prerequisite checks (proxy + tech stack validation). + +$PROXY_TEST_URL = "https://www.browserstack.com/automate/browsers.json" + +function Parse-ProxyUrl { + param([string]$ProxyUrl) + if ([string]::IsNullOrWhiteSpace($ProxyUrl)) { + return $null + } + + $cleaned = $ProxyUrl -replace '^https?://', '' + if ($cleaned -match '@') { + $cleaned = $cleaned.Substring($cleaned.IndexOf('@') + 1) + } + + if ($cleaned -match '^([^:]+):(\d+)') { + return @{ + Host = $matches[1] + Port = $matches[2] + } + } elseif ($cleaned -match '^([^:]+)') { + return @{ + Host = $matches[1] + Port = "8080" + } + } + return $null +} + +function Set-ProxyInEnv { + param( + [string]$Username, + [string]$AccessKey + ) + + Log-Section "🌐 Network & Proxy Validation" $GLOBAL_LOG + + $proxy = $env:http_proxy + if ([string]::IsNullOrWhiteSpace($proxy)) { $proxy = $env:HTTP_PROXY } + if ([string]::IsNullOrWhiteSpace($proxy)) { $proxy = $env:https_proxy } + if ([string]::IsNullOrWhiteSpace($proxy)) { $proxy = $env:HTTPS_PROXY } + + $env:PROXY_HOST = "" + $env:PROXY_PORT = "" + + if ([string]::IsNullOrWhiteSpace($proxy)) { + Log-Line "No proxy found in environment. Using direct connection." $GLOBAL_LOG + return + } + + Log-Line "Proxy detected: $proxy" $GLOBAL_LOG + $proxyInfo = Parse-ProxyUrl -ProxyUrl $proxy + if (-not $proxyInfo) { + Log-Line "❌ Failed to parse proxy URL: $proxy" $GLOBAL_LOG + return + } + + $pair = if ($Username -and $AccessKey) { "$Username`:$AccessKey" } else { "" } + $base64Creds = "" + if ($pair) { + $base64Creds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair)) + } + + try { + $proxyUri = "http://$($proxyInfo.Host):$($proxyInfo.Port)" + $webProxy = New-Object System.Net.WebProxy($proxyUri) + $webClient = New-Object System.Net.WebClient + $webClient.Proxy = $webProxy + if ($base64Creds) { + $webClient.Headers.Add("Authorization", "Basic $base64Creds") + } + + $null = $webClient.DownloadString($PROXY_TEST_URL) + + Log-Line "✅ Reachable via proxy. HTTP 200" $GLOBAL_LOG + Log-Line "Exporting PROXY_HOST=$($proxyInfo.Host)" $GLOBAL_LOG + Log-Line "Exporting PROXY_PORT=$($proxyInfo.Port)" $GLOBAL_LOG + $env:PROXY_HOST = $proxyInfo.Host + $env:PROXY_PORT = $proxyInfo.Port + } catch { + $statusMsg = $_.Exception.Message + Log-Line "❌ Not reachable via proxy. Error: $statusMsg" $GLOBAL_LOG + $env:PROXY_HOST = "" + $env:PROXY_PORT = "" + } +} + +function Validate-Tech-Stack { + Log-Line "ℹ️ Checking prerequisites for $script:TECH_STACK" $GLOBAL_LOG + switch ($script:TECH_STACK) { + "Java" { + if (-not (Get-Command java -ErrorAction SilentlyContinue)) { + Log-Line "❌ Java command not found in PATH." $GLOBAL_LOG + throw "Java not found" + } + $verInfo = & cmd /c 'java -version 2>&1' + if (-not $verInfo) { + Log-Line "❌ Java exists but failed to run." $GLOBAL_LOG + throw "Java invocation failed" + } + Log-Line "✅ Java is installed. Version details:" $GLOBAL_LOG + ($verInfo -split "`r?`n") | ForEach-Object { if ($_ -ne "") { Log-Line " $_" $GLOBAL_LOG } } + } + "Python" { + try { + Set-PythonCmd + $code = Invoke-Py -Arguments @("--version") -LogFile $null -WorkingDirectory (Get-Location).Path + if ($code -eq 0) { + Log-Line ("✅ Python3 is installed: {0}" -f ( ($PY_CMD -join ' ') )) $GLOBAL_LOG + } else { + throw "Python present but failed to execute" + } + } catch { + Log-Line "❌ Python3 exists but failed to run." $GLOBAL_LOG + throw + } + } + "NodeJS" { + if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Log-Line "❌ Node.js command not found in PATH." $GLOBAL_LOG + throw "Node not found" + } + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Log-Line "❌ npm command not found in PATH." $GLOBAL_LOG + throw "npm not found" + } + $nodeVer = & node -v 2>&1 + if (-not $nodeVer) { + Log-Line "❌ Node.js exists but failed to run." $GLOBAL_LOG + throw "Node.js invocation failed" + } + $npmVer = & npm -v 2>&1 + if (-not $npmVer) { + Log-Line "❌ npm exists but failed to run." $GLOBAL_LOG + throw "npm invocation failed" + } + Log-Line "✅ Node.js is installed: $nodeVer" $GLOBAL_LOG + Log-Line "✅ npm is installed: $npmVer" $GLOBAL_LOG + } + default { + Log-Line "❌ Unknown TECH_STACK: $script:TECH_STACK" $GLOBAL_LOG + throw "Unknown tech stack" + } + } +} + + + diff --git a/win/env-setup-run.ps1 b/win/env-setup-run.ps1 new file mode 100644 index 0000000..bc99213 --- /dev/null +++ b/win/env-setup-run.ps1 @@ -0,0 +1,589 @@ +# Environment Setup and Run functions for Windows BrowserStack NOW. +# Mirrors the Mac env-setup-run.sh structure. + +# ===== Setup: Web (Java) ===== +function Setup-Web-Java { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-testng-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + Push-Location $TARGET + try { + Log-Line "ℹ️ Target website: $CX_TEST_URL" $GLOBAL_LOG + + if (Test-DomainPrivate) { + $UseLocal = $true + } + + Report-BStackLocalStatus -LocalFlag $UseLocal + + Log-Line "🧩 Generating YAML config (browserstack.yml)" $GLOBAL_LOG + $platforms = Generate-Web-Platforms-Yaml -MaxTotalParallels $ParallelsPerPlatform + $localFlag = if ($UseLocal) { "true" } else { "false" } + + $yamlContent = @" +userName: $BROWSERSTACK_USERNAME +accessKey: $BROWSERSTACK_ACCESS_KEY +framework: testng +browserstackLocal: $localFlag +buildName: now-$NOW_OS-web-java-testng +projectName: now-$NOW_OS-web +percy: true +accessibility: true +platforms: +$platforms +parallelsPerPlatform: $ParallelsPerPlatform +"@ + + Set-Content "browserstack.yml" -Value $yamlContent + Log-Line "✅ Created browserstack.yml in root directory" $GLOBAL_LOG + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: now-$NOW_OS-web-java-testng" $GLOBAL_LOG + Log-Line "ℹ️ Web Application Endpoint: $CX_TEST_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $localFlag" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms:" $GLOBAL_LOG + $platforms -split "`n" | ForEach-Object { if ($_.Trim()) { Log-Line " $_" $GLOBAL_LOG } } + + $mvn = Get-MavenCommand -RepoDir $TARGET + Log-Line "⚙️ Running '$mvn install -DskipTests'" $GLOBAL_LOG + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + [void](Invoke-External -Exe $mvn -Arguments @("install","-DskipTests") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + Print-TestsRunningSection -Command "mvn test -P sample-test" + [void](Invoke-External -Exe $mvn -Arguments @("test","-P","sample-test") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Setup: Web (Python) ===== +function Setup-Web-Python { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-pytest-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + Push-Location $TARGET + try { + if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } + $venv = Join-Path $TARGET "venv" + if (!(Test-Path $venv)) { + [void](Invoke-Py -Arguments @("-m","venv",$venv) -LogFile $LogFile -WorkingDirectory $TARGET) + } + $venvPy = Get-VenvPython -VenvDir $venv + + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + [void](Invoke-External -Exe $venvPy -Arguments @("-m","pip","install","-r","requirements.txt") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + $env:PATH = (Join-Path $venv 'Scripts') + ";" + $env:PATH + $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME + $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY + + if (Test-DomainPrivate) { + $UseLocal = $true + } + + Report-BStackLocalStatus -LocalFlag $UseLocal + + $env:BROWSERSTACK_CONFIG_FILE = "browserstack.yml" + $platforms = Generate-Web-Platforms-Yaml -MaxTotalParallels $ParallelsPerPlatform + $localFlag = if ($UseLocal) { "true" } else { "false" } + +@" +userName: $BROWSERSTACK_USERNAME +accessKey: $BROWSERSTACK_ACCESS_KEY +framework: pytest +browserstackLocal: $localFlag +buildName: now-$NOW_OS-web-python-pytest +projectName: now-$NOW_OS-web +percy: true +accessibility: true +platforms: +$platforms +parallelsPerPlatform: $ParallelsPerPlatform +"@ | Set-Content "browserstack.yml" + + Log-Line "✅ Updated browserstack.yml with platforms and credentials" $GLOBAL_LOG + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: now-$NOW_OS-web-python-pytest" $GLOBAL_LOG + Log-Line "ℹ️ Web Application Endpoint: $CX_TEST_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $localFlag" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms:" $GLOBAL_LOG + $platforms -split "`n" | ForEach-Object { if ($_.Trim()) { Log-Line " $_" $GLOBAL_LOG } } + + $sdk = Join-Path $venv "Scripts\browserstack-sdk.exe" + Print-TestsRunningSection -Command "browserstack-sdk pytest -s tests/bstack-sample-test.py" + [void](Invoke-External -Exe $sdk -Arguments @('pytest','-s','tests/bstack-sample-test.py') -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Setup: Web (NodeJS) ===== +function Setup-Web-NodeJS { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-webdriverio-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + Push-Location $TARGET + try { + Log-Line "⚙️ Running 'npm install'" $GLOBAL_LOG + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","install") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + $caps = Generate-Web-Caps-Json -MaxTotalParallels $ParallelsPerPlatform + $env:BSTACK_PARALLELS = $ParallelsPerPlatform + $env:BSTACK_CAPS_JSON = $caps + + if (Test-DomainPrivate) { + $UseLocal = $true + } + + Report-BStackLocalStatus -LocalFlag $UseLocal + + $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME + $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY + $localFlagStr = if ($UseLocal) { "true" } else { "false" } + $env:BROWSERSTACK_LOCAL = $localFlagStr + $env:BROWSERSTACK_BUILD_NAME = "now-$NOW_OS-web-nodejs-wdio" + $env:BROWSERSTACK_PROJECT_NAME = "now-$NOW_OS-web" + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: $($env:BROWSERSTACK_BUILD_NAME)" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Project: $($env:BROWSERSTACK_PROJECT_NAME)" $GLOBAL_LOG + Log-Line "ℹ️ Web Application Endpoint: $CX_TEST_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $localFlagStr" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms:" $GLOBAL_LOG + Log-Line " $caps" $GLOBAL_LOG + + Print-TestsRunningSection -Command "npm run test" + [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","run","test") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Setup: Mobile (Java) ===== +function Setup-Mobile-Java { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-testng-appium-app-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + Push-Location $TARGET + try { + if ($APP_PLATFORM -eq "all" -or $APP_PLATFORM -eq "android") { + Set-Location "android\testng-examples" + } else { + Set-Location "ios\testng-examples" + } + + $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME + $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY + $env:BROWSERSTACK_CONFIG_FILE = ".\browserstack.yml" + + $platforms = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $ParallelsPerPlatform + $localFlag = if ($UseLocal) { "true" } else { "false" } + + # Write complete browserstack.yml (not just append) + $yamlContent = @" +userName: $BROWSERSTACK_USERNAME +accessKey: $BROWSERSTACK_ACCESS_KEY +framework: testng +browserstackLocal: $localFlag +buildName: now-$NOW_OS-app-java-testng +projectName: now-$NOW_OS-app +parallelsPerPlatform: $ParallelsPerPlatform +app: $APP_URL +platforms: +$platforms +"@ + $yamlContent | Set-Content -Path $env:BROWSERSTACK_CONFIG_FILE -Encoding UTF8 + + Report-BStackLocalStatus -LocalFlag $UseLocal + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: now-$NOW_OS-app-java-testng" $GLOBAL_LOG + Log-Line "ℹ️ Native App Endpoint: $APP_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $localFlag" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms:" $GLOBAL_LOG + $platforms -split "`n" | ForEach-Object { if ($_.Trim()) { Log-Line " $_" $GLOBAL_LOG } } + + $mvn = Get-MavenCommand -RepoDir (Get-Location).Path + Log-Line "⚙️ Running '$mvn clean'" $GLOBAL_LOG + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + $cleanExit = Invoke-External -Exe $mvn -Arguments @("clean") -LogFile $LogFile -WorkingDirectory (Get-Location).Path + if ($cleanExit -ne 0) { + Log-Line "❌ 'mvn clean' FAILED. See $LogFile for details." $GLOBAL_LOG + throw "Maven clean failed" + } + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + Print-TestsRunningSection -Command "mvn test -P sample-test" + [void](Invoke-External -Exe $mvn -Arguments @("test","-P","sample-test") -LogFile $LogFile -WorkingDirectory (Get-Location).Path) + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Setup: Mobile (Python) ===== +function Setup-Mobile-Python { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-pytest-appium-app-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + Push-Location $TARGET + try { + if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } + $venv = Join-Path $TARGET "venv" + if (!(Test-Path $venv)) { + [void](Invoke-Py -Arguments @("-m","venv",$venv) -LogFile $LogFile -WorkingDirectory $TARGET) + } + $venvPy = Get-VenvPython -VenvDir $venv + + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + [void](Invoke-External -Exe $venvPy -Arguments @("-m","pip","install","-r","requirements.txt") -LogFile $LogFile -WorkingDirectory $TARGET) + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + $env:PATH = (Join-Path $venv 'Scripts') + ";" + $env:PATH + $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME + $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY + $env:BROWSERSTACK_APP = $APP_URL + + $originalPlatform = $APP_PLATFORM + $localFlag = if ($UseLocal) { "true" } else { "false" } + + # Generate platform YAMLs + $script:APP_PLATFORM = "android" + $platformYamlAndroid = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $ParallelsPerPlatform + $androidYmlPath = Join-Path $TARGET "android\browserstack.yml" +@" +userName: $BROWSERSTACK_USERNAME +accessKey: $BROWSERSTACK_ACCESS_KEY +framework: pytest +browserstackLocal: $localFlag +buildName: now-$NOW_OS-app-python-pytest +projectName: now-$NOW_OS-app +parallelsPerPlatform: $ParallelsPerPlatform +app: $APP_URL +platforms: +$platformYamlAndroid +"@ | Set-Content $androidYmlPath + + $script:APP_PLATFORM = "ios" + $platformYamlIos = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $ParallelsPerPlatform + $iosYmlPath = Join-Path $TARGET "ios\browserstack.yml" +@" +userName: $BROWSERSTACK_USERNAME +accessKey: $BROWSERSTACK_ACCESS_KEY +framework: pytest +browserstackLocal: $localFlag +buildName: now-$NOW_OS-app-python-pytest +projectName: now-$NOW_OS-app +parallelsPerPlatform: $ParallelsPerPlatform +app: $APP_URL +platforms: +$platformYamlIos +"@ | Set-Content $iosYmlPath + + $script:APP_PLATFORM = $originalPlatform + Log-Line "✅ Wrote platform YAMLs" $GLOBAL_LOG + + $runDirName = if ($APP_PLATFORM -eq "ios") { "ios" } else { "android" } + $runDir = Join-Path $TARGET $runDirName + $platformYaml = if ($runDirName -eq "ios") { $platformYamlIos } else { $platformYamlAndroid } + + Report-BStackLocalStatus -LocalFlag $UseLocal + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: now-$NOW_OS-app-python-pytest" $GLOBAL_LOG + Log-Line "ℹ️ Native App Endpoint: $APP_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $localFlag" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms:" $GLOBAL_LOG + $platformYaml -split "`n" | ForEach-Object { if ($_.Trim()) { Log-Line " $_" $GLOBAL_LOG } } + + $sdk = Join-Path $venv "Scripts\browserstack-sdk.exe" + Print-TestsRunningSection -Command "cd $runDirName && browserstack-sdk pytest -s bstack_sample.py" + + Push-Location $runDir + try { + [void](Invoke-External -Exe $sdk -Arguments @('pytest','-s','bstack_sample.py') -LogFile $LogFile -WorkingDirectory (Get-Location).Path) + } finally { + Pop-Location + } + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Setup: Mobile (NodeJS) ===== +function Setup-Mobile-NodeJS { + param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) + + $REPO = "now-webdriverio-appium-app-browserstack" + $TARGET = Join-Path $GLOBAL_DIR $REPO + + New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null + if (Test-Path $TARGET) { + Remove-Item -Path $TARGET -Recurse -Force + } + + Log-Line "ℹ️ Cloning repository: $REPO" $GLOBAL_LOG + Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile (Get-RunLogFile) + + $testDir = Join-Path $TARGET "test" + Push-Location $testDir + try { + Log-Line "⚙️ Running 'npm install'" $GLOBAL_LOG + Log-Line "ℹ️ Installing dependencies" $GLOBAL_LOG + [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","install") -LogFile $LogFile -WorkingDirectory $testDir) + Log-Line "✅ Dependencies installed" $GLOBAL_LOG + + # Generate capabilities JSON and set as environment variable (like Mac) + $capsJson = Generate-Mobile-Caps-Json-String -MaxTotalParallels $ParallelsPerPlatform + + $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME + $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY + $env:BSTACK_PARALLELS = $ParallelsPerPlatform + $env:BSTACK_CAPS_JSON = $capsJson + $env:BROWSERSTACK_APP = $APP_URL + $env:BROWSERSTACK_BUILD_NAME = "now-$NOW_OS-app-nodejs-wdio" + $env:BROWSERSTACK_PROJECT_NAME = "now-$NOW_OS-app" + $env:BROWSERSTACK_LOCAL = "true" + + # Validate Environment Variables + Log-Section "Validate Environment Variables" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Username: $BROWSERSTACK_USERNAME" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Build: $($env:BROWSERSTACK_BUILD_NAME)" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Project: $($env:BROWSERSTACK_PROJECT_NAME)" $GLOBAL_LOG + Log-Line "ℹ️ Native App Endpoint: $APP_URL" $GLOBAL_LOG + Log-Line "ℹ️ BrowserStack Local Flag: $($env:BROWSERSTACK_LOCAL)" $GLOBAL_LOG + Log-Line "ℹ️ Parallels per platform: $ParallelsPerPlatform" $GLOBAL_LOG + Log-Line "ℹ️ Platforms: $capsJson" $GLOBAL_LOG + + Print-TestsRunningSection -Command "npm run test" + [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","run","test") -LogFile $LogFile -WorkingDirectory $testDir) + Log-Line "ℹ️ Run Test command completed." $GLOBAL_LOG + + } finally { + Pop-Location + Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) + } +} + +# ===== Helper Functions ===== +function Report-BStackLocalStatus { + param([bool]$LocalFlag) + if ($LocalFlag) { + Log-Line "✅ Target website is behind firewall. BrowserStack Local enabled for this run." $GLOBAL_LOG + } else { + Log-Line "✅ Target website is publicly resolvable. BrowserStack Local disabled for this run." $GLOBAL_LOG + } +} + +function Print-TestsRunningSection { + param([string]$Command) + Log-Section "🚀 Running Tests: $Command" $GLOBAL_LOG + Log-Line "ℹ️ Executing: Test run command. This could take a few minutes..." $GLOBAL_LOG + Log-Line "ℹ️ You can monitor test progress here: 🔗 https://automation.browserstack.com/" $GLOBAL_LOG +} + +function Identify-RunStatus-Java { + param([string]$LogFile) + if (!(Test-Path $LogFile)) { return $false } + $content = Get-Content $LogFile -Raw + $match = [regex]::Match($content, 'Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)') + if (-not $match.Success) { return $false } + $passed = [int]$match.Groups[1].Value - ([int]$match.Groups[2].Value + [int]$match.Groups[3].Value + [int]$match.Groups[4].Value) + if ($passed -gt 0) { + Log-Line "✅ Success: $passed test(s) passed." $GLOBAL_LOG + return $true + } + return $false +} + +function Identify-RunStatus-Python { + param([string]$LogFile) + if (!(Test-Path $LogFile)) { return $false } + $content = Get-Content $LogFile -Raw + $matches = [regex]::Matches($content, '(\d+)\s+passed') + $passedSum = 0 + foreach ($m in $matches) { $passedSum += [int]$m.Groups[1].Value } + if ($passedSum -gt 0) { + Log-Line "✅ Success: $passedSum test(s) passed." $GLOBAL_LOG + return $true + } + return $false +} + +function Identify-RunStatus-NodeJS { + param([string]$LogFile) + if (!(Test-Path $LogFile)) { return $false } + $content = Get-Content $LogFile -Raw + $match = [regex]::Match($content, '(\d+)\s+pass') + if ($match.Success -and [int]$match.Groups[1].Value -gt 0) { + Log-Line "✅ Success: $($match.Groups[1].Value) test(s) passed." $GLOBAL_LOG + return $true + } + return $false +} + +# ===== Setup Environment Wrapper ===== +function Setup-Environment { + param( + [Parameter(Mandatory)][string]$SetupType, + [Parameter(Mandatory)][string]$TechStack, + [string]$RunMode = "--interactive" + ) + + Log-Section "📦 Project Setup" $GLOBAL_LOG + + $maxParallels = if ($SetupType -match "web") { $TEAM_PARALLELS_MAX_ALLOWED_WEB } else { $TEAM_PARALLELS_MAX_ALLOWED_MOBILE } + Log-Line "Team max parallels: $maxParallels" $GLOBAL_LOG + + $localFlag = $false + $totalParallels = [int]([Math]::Floor($maxParallels * $PARALLEL_PERCENTAGE)) + if ($totalParallels -lt 1) { $totalParallels = 1 } + + if ($RunMode -match "--silent" -and $totalParallels -gt 5) { + $originalParallels = $totalParallels + $totalParallels = 5 + Log-Line "ℹ️ Silent mode: capping parallels per platform to $totalParallels (requested $originalParallels)" $GLOBAL_LOG + } + + Log-Line "Total parallels allocated: $totalParallels" $GLOBAL_LOG + + $success = $false + $logFile = Get-RunLogFile + + switch ($TechStack) { + "Java" { + if ($SetupType -match "web") { + Setup-Web-Java -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-Java -LogFile $logFile + } else { + Setup-Mobile-Java -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-Java -LogFile $logFile + } + } + "Python" { + if ($SetupType -match "web") { + Setup-Web-Python -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-Python -LogFile $logFile + } else { + Setup-Mobile-Python -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-Python -LogFile $logFile + } + } + "NodeJS" { + if ($SetupType -match "web") { + Setup-Web-NodeJS -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-NodeJS -LogFile $logFile + } else { + Setup-Mobile-NodeJS -UseLocal:$localFlag -ParallelsPerPlatform $totalParallels -LogFile $logFile + $success = Identify-RunStatus-NodeJS -LogFile $logFile + } + } + default { + Log-Line "⚠️ Unknown TECH_STACK: $TechStack" $GLOBAL_LOG + return + } + } + + Log-Section "✅ Results" $GLOBAL_LOG + if ($success) { + Log-Line "✅ $SetupType setup succeeded." $GLOBAL_LOG + } else { + Log-Line "❌ $SetupType setup ended. Check $logFile for details." $GLOBAL_LOG + } +} + +# ===== Run Setup Wrapper (like Mac's run_setup) ===== +function Run-Setup { + param( + [string]$TestType, + [string]$TechStack, + [string]$RunMode = "--interactive" + ) + Setup-Environment -SetupType $TestType -TechStack $TechStack -RunMode $RunMode +} + + diff --git a/win/logging-utils.ps1 b/win/logging-utils.ps1 new file mode 100644 index 0000000..68da254 --- /dev/null +++ b/win/logging-utils.ps1 @@ -0,0 +1,56 @@ +# Logging helpers shared across the Windows BrowserStack NOW scripts. + +if (-not (Get-Variable -Name NOW_RUN_LOG_FILE -Scope Script -ErrorAction SilentlyContinue)) { + $script:NOW_RUN_LOG_FILE = "" +} + +function Set-RunLogFile { + param([string]$Path) + $script:NOW_RUN_LOG_FILE = $Path + if ($Path) { + $env:NOW_RUN_LOG_FILE = $Path + } else { + Remove-Item Env:NOW_RUN_LOG_FILE -ErrorAction SilentlyContinue + } +} + +function Get-RunLogFile { + return $script:NOW_RUN_LOG_FILE +} + +function Log-Line { + param( + [Parameter(Mandatory=$true)][AllowEmptyString()][string]$Message, + [string]$DestFile + ) + if (-not $DestFile) { + $DestFile = Get-RunLogFile + } + + $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + $line = "[$ts] $Message" + Write-Host $line + if ($DestFile) { + $dir = Split-Path -Parent $DestFile + if ($dir -and !(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } + Add-Content -Path $DestFile -Value $line + } +} + +function Log-Section { + param( + [Parameter(Mandatory)][AllowEmptyString()][string]$Title, + [string]$DestFile + ) + $divider = "───────────────────────────────────────────────" + Log-Line "" $DestFile + Log-Line $divider $DestFile + Log-Line ("{0}" -f $Title) $DestFile + Log-Line $divider $DestFile +} + +function Log-Info { param([string]$Message,[string]$DestFile) Log-Line ("ℹ️ $Message") $DestFile } +function Log-Success { param([string]$Message,[string]$DestFile) Log-Line ("✅ $Message") $DestFile } +function Log-Warn { param([string]$Message,[string]$DestFile) Log-Line ("⚠️ $Message") $DestFile } +function Log-Error { param([string]$Message,[string]$DestFile) Log-Line ("❌ $Message") $DestFile } + diff --git a/win/proxy-check.ps1 b/win/proxy-check.ps1 deleted file mode 100644 index bc47faa..0000000 --- a/win/proxy-check.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -#requires -version 5.0 -<# - BrowserStack Proxy Detection and Validation - - Detects proxy from environment variables - - Tests BrowserStack API connectivity through proxy - - Exports PROXY_HOST and PROXY_PORT if successful -#> - -param( - [string]$BrowserStackUsername, - [string]$BrowserStackAccessKey -) - -$ErrorActionPreference = 'Continue' - -# Test URL for connectivity check -$TEST_URL = "https://www.browserstack.com/automate/browsers.json" - -# Function to parse proxy URL -function Parse-ProxyUrl { - param([string]$ProxyUrl) - - if ([string]::IsNullOrWhiteSpace($ProxyUrl)) { - return $null - } - - # Remove protocol (http:// or https://) - $cleaned = $ProxyUrl -replace '^https?://', '' - - # Remove credentials if present (user:pass@) - if ($cleaned -match '@') { - $cleaned = $cleaned.Substring($cleaned.IndexOf('@') + 1) - } - - # Extract host and port - if ($cleaned -match '^([^:]+):(\d+)') { - return @{ - Host = $matches[1] - Port = $matches[2] - } - } elseif ($cleaned -match '^([^:]+)') { - # No port specified, use default - return @{ - Host = $matches[1] - Port = "8080" # default proxy port - } - } - - return $null -} - -# Detect proxy from environment variables (case-insensitive) -$PROXY = $env:http_proxy -if ([string]::IsNullOrWhiteSpace($PROXY)) { $PROXY = $env:HTTP_PROXY } -if ([string]::IsNullOrWhiteSpace($PROXY)) { $PROXY = $env:https_proxy } -if ([string]::IsNullOrWhiteSpace($PROXY)) { $PROXY = $env:HTTPS_PROXY } - -# Reset output variables -$env:PROXY_HOST = "" -$env:PROXY_PORT = "" - -# If no proxy configured, exit early -if ([string]::IsNullOrWhiteSpace($PROXY)) { - Write-Host "No proxy found in environment. Clearing proxy host and port variables." - $env:PROXY_HOST = "" - $env:PROXY_PORT = "" - exit 0 -} - -Write-Host "Proxy detected: $PROXY" - -# Parse proxy URL -$proxyInfo = Parse-ProxyUrl -ProxyUrl $PROXY -if (-not $proxyInfo) { - Write-Host "❌ Failed to parse proxy URL: $PROXY" - $env:PROXY_HOST = "" - $env:PROXY_PORT = "" - exit 1 -} - -Write-Host "Testing reachability via proxy..." -Write-Host " Proxy Host: $($proxyInfo.Host)" -Write-Host " Proxy Port: $($proxyInfo.Port)" - -# Encode BrowserStack credentials in Base64 -$base64Creds = "" -if (-not [string]::IsNullOrWhiteSpace($BrowserStackUsername) -and - -not [string]::IsNullOrWhiteSpace($BrowserStackAccessKey)) { - $pair = "${BrowserStackUsername}:${BrowserStackAccessKey}" - $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair) - $base64Creds = [System.Convert]::ToBase64String($bytes) -} - -# Test connectivity through proxy -try { - $proxyUri = "http://$($proxyInfo.Host):$($proxyInfo.Port)" - - # Create web request with proxy - $webProxy = New-Object System.Net.WebProxy($proxyUri) - $webClient = New-Object System.Net.WebClient - $webClient.Proxy = $webProxy - - # Add authorization header if credentials provided - if (-not [string]::IsNullOrWhiteSpace($base64Creds)) { - $webClient.Headers.Add("Authorization", "Basic $base64Creds") - } - - # Attempt to download (with timeout) - $null = $webClient.DownloadString($TEST_URL) - - # If we reach here, the request succeeded - Write-Host "✅ Reachable. HTTP 200" - Write-Host "Exporting PROXY_HOST=$($proxyInfo.Host)" - Write-Host "Exporting PROXY_PORT=$($proxyInfo.Port)" - - $env:PROXY_HOST = $proxyInfo.Host - $env:PROXY_PORT = $proxyInfo.Port - - exit 0 - -} catch { - $statusCode = "Unknown" - if ($_.Exception.InnerException -and $_.Exception.InnerException.Response) { - $statusCode = [int]$_.Exception.InnerException.Response.StatusCode - } - - Write-Host "❌ Not reachable (HTTP $statusCode). Clearing variables." - Write-Host " Error: $($_.Exception.Message)" - - $env:PROXY_HOST = "" - $env:PROXY_PORT = "" - - exit 0 # Exit successfully even if proxy check fails -} - diff --git a/win/run.ps1 b/win/run.ps1 index 3ba470d..8003d2b 100644 --- a/win/run.ps1 +++ b/win/run.ps1 @@ -1,1749 +1,108 @@ -#requires -version 5.0 +#requires -version 5.0 <# BrowserStack Onboarding (PowerShell 5.0, GUI) - - Full parity port of macOS bash + - Full parity port of macOS bash run.sh - Uses WinForms for GUI prompts - Logs to %USERPROFILE%\.browserstack\NOW\logs #> +param( + [string]$RunMode = "--interactive", + [string]$TT, + [string]$TSTACK, + [string]$TestUrl, + [string]$AppPath, + [string]$AppPlatform +) + Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing -# ===== Global Variables ===== -$WORKSPACE_DIR = Join-Path $env:USERPROFILE ".browserstack" -$PROJECT_FOLDER = "NOW" - -$GLOBAL_DIR = Join-Path $WORKSPACE_DIR $PROJECT_FOLDER -$LOG_DIR = Join-Path $GLOBAL_DIR "logs" -$GLOBAL_LOG = Join-Path $LOG_DIR "global.log" -$WEB_LOG = Join-Path $LOG_DIR "web_run_result.log" -$MOBILE_LOG = Join-Path $LOG_DIR "mobile_run_result.log" - -# Clear/prepare logs -if (!(Test-Path $LOG_DIR)) { New-Item -ItemType Directory -Path $LOG_DIR | Out-Null } -'' | Out-File -FilePath $GLOBAL_LOG -Encoding UTF8 -'' | Out-File -FilePath $WEB_LOG -Encoding UTF8 -'' | Out-File -FilePath $MOBILE_LOG -Encoding UTF8 - -# Script state -$BROWSERSTACK_USERNAME = "" -$BROWSERSTACK_ACCESS_KEY = "" -$TEST_TYPE = "" # Web / App / Both -$TECH_STACK = "" # Java / Python / JS -[double]$PARALLEL_PERCENTAGE = 1.00 - -$WEB_PLAN_FETCHED = $false -$MOBILE_PLAN_FETCHED = $false -[int]$TEAM_PARALLELS_MAX_ALLOWED_WEB = 0 -[int]$TEAM_PARALLELS_MAX_ALLOWED_MOBILE = 0 - -# URL handling -$DEFAULT_TEST_URL = "https://bstackdemo.com" -$CX_TEST_URL = $DEFAULT_TEST_URL - -# App handling -$APP_URL = "" -$APP_PLATFORM = "" # ios | android | all - -# Chosen Python command tokens (set during validation when Python is selected) -$PY_CMD = @() - -# ===== Error patterns (placeholders to match your original arrays) ===== -$WEB_SETUP_ERRORS = @("") -$WEB_LOCAL_ERRORS = @("") -$MOBILE_SETUP_ERRORS= @("") -$MOBILE_LOCAL_ERRORS= @("") - -# ===== Example Platform Templates ===== -$WEB_PLATFORM_TEMPLATES = @( - "Windows|10|Chrome", - "Windows|10|Firefox", - "Windows|11|Edge", - "Windows|11|Chrome", - "Windows|8|Chrome", - "OS X|Monterey|Chrome", - "OS X|Ventura|Chrome", - "OS X|Catalina|Firefox" -) - -# Mobile tiers (kept for parity) -$MOBILE_TIER1 = @( - "ios|iPhone 15|17", - "ios|iPhone 15 Pro|17", - "ios|iPhone 16|18", - "android|Samsung Galaxy S25|15", - "android|Samsung Galaxy S24|14" -) -$MOBILE_TIER2 = @( - "ios|iPhone 14 Pro|16", - "ios|iPhone 14|16", - "ios|iPad Air 13 2025|18", - "android|Samsung Galaxy S23|13", - "android|Samsung Galaxy S22|12", - "android|Samsung Galaxy S21|11", - "android|Samsung Galaxy Tab S10 Plus|15" -) -$MOBILE_TIER3 = @( - "ios|iPhone 13 Pro Max|15", - "ios|iPhone 13|15", - "ios|iPhone 12 Pro|14", - "ios|iPhone 12 Pro|17", - "ios|iPhone 12|17", - "ios|iPhone 12|14", - "ios|iPhone 12 Pro Max|16", - "ios|iPhone 13 Pro|15", - "ios|iPhone 13 Mini|15", - "ios|iPhone 16 Pro|18", - "ios|iPad 9th|15", - "ios|iPad Pro 12.9 2020|14", - "ios|iPad Pro 12.9 2020|16", - "ios|iPad 8th|16", - "android|Samsung Galaxy S22 Ultra|12", - "android|Samsung Galaxy S21|12", - "android|Samsung Galaxy S21 Ultra|11", - "android|Samsung Galaxy S20|10", - "android|Samsung Galaxy M32|11", - "android|Samsung Galaxy Note 20|10", - "android|Samsung Galaxy S10|9", - "android|Samsung Galaxy Note 9|8", - "android|Samsung Galaxy Tab S8|12", - "android|Google Pixel 9|15", - "android|Google Pixel 6 Pro|13", - "android|Google Pixel 8|14", - "android|Google Pixel 7|13", - "android|Google Pixel 6|12", - "android|Vivo Y21|11", - "android|Vivo Y50|10", - "android|Oppo Reno 6|11" -) -$MOBILE_TIER4 = @( - "ios|iPhone 15 Pro Max|17", - "ios|iPhone 15 Pro Max|26", - "ios|iPhone 15|26", - "ios|iPhone 15 Plus|17", - "ios|iPhone 14 Pro|26", - "ios|iPhone 14|18", - "ios|iPhone 14|26", - "ios|iPhone 13 Pro Max|18", - "ios|iPhone 13|16", - "ios|iPhone 13|17", - "ios|iPhone 13|18", - "ios|iPhone 12 Pro|18", - "ios|iPhone 14 Pro Max|16", - "ios|iPhone 14 Plus|16", - "ios|iPhone 11|13", - "ios|iPhone 8|11", - "ios|iPhone 7|10", - "ios|iPhone 17 Pro Max|26", - "ios|iPhone 17 Pro|26", - "ios|iPhone 17 Air|26", - "ios|iPhone 17|26", - "ios|iPhone 16e|18", - "ios|iPhone 16 Pro Max|18", - "ios|iPhone 16 Plus|18", - "ios|iPhone SE 2020|16", - "ios|iPhone SE 2022|15", - "ios|iPad Air 4|14", - "ios|iPad 9th|18", - "ios|iPad Air 5|26", - "ios|iPad Pro 11 2021|18", - "ios|iPad Pro 13 2024|17", - "ios|iPad Pro 12.9 2021|14", - "ios|iPad Pro 12.9 2021|17", - "ios|iPad Pro 11 2024|17", - "ios|iPad Air 6|17", - "ios|iPad Pro 12.9 2022|16", - "ios|iPad Pro 11 2022|16", - "ios|iPad 10th|16", - "ios|iPad Air 13 2025|26", - "ios|iPad Pro 11 2020|13", - "ios|iPad Pro 11 2020|16", - "ios|iPad 8th|14", - "ios|iPad Mini 2021|15", - "ios|iPad Pro 12.9 2018|12", - "ios|iPad 6th|11", - "android|Samsung Galaxy S23 Ultra|13", - "android|Samsung Galaxy S22 Plus|12", - "android|Samsung Galaxy S21 Plus|11", - "android|Samsung Galaxy S20 Ultra|10", - "android|Samsung Galaxy S25 Ultra|15", - "android|Samsung Galaxy S24 Ultra|14", - "android|Samsung Galaxy M52|11", - "android|Samsung Galaxy A52|11", - "android|Samsung Galaxy A51|10", - "android|Samsung Galaxy A11|10", - "android|Samsung Galaxy A10|9", - "android|Samsung Galaxy Tab A9 Plus|14", - "android|Samsung Galaxy Tab S9|13", - "android|Samsung Galaxy Tab S7|10", - "android|Samsung Galaxy Tab S7|11", - "android|Samsung Galaxy Tab S6|9", - "android|Google Pixel 9|16", - "android|Google Pixel 10 Pro XL|16", - "android|Google Pixel 10 Pro|16", - "android|Google Pixel 10|16", - "android|Google Pixel 9 Pro XL|15", - "android|Google Pixel 9 Pro|15", - "android|Google Pixel 6 Pro|12", - "android|Google Pixel 6 Pro|15", - "android|Google Pixel 8 Pro|14", - "android|Google Pixel 7 Pro|13", - "android|Google Pixel 5|11", - "android|OnePlus 13R|15", - "android|OnePlus 12R|14", - "android|OnePlus 11R|13", - "android|OnePlus 9|11", - "android|OnePlus 8|10", - "android|Motorola Moto G71 5G|11", - "android|Motorola Moto G9 Play|10", - "android|Vivo V21|11", - "android|Oppo A96|11", - "android|Oppo Reno 3 Pro|10", - "android|Xiaomi Redmi Note 11|11", - "android|Xiaomi Redmi Note 9|10", - "android|Huawei P30|9" -) - -# MOBILE_ALL combines the tiers -$MOBILE_ALL = @() -$MOBILE_ALL += $MOBILE_TIER1 -$MOBILE_ALL += $MOBILE_TIER2 -$MOBILE_ALL += $MOBILE_TIER3 -$MOBILE_ALL += $MOBILE_TIER4 - -# ===== Helpers ===== -function Log-Line { - param( - [Parameter(Mandatory=$true)][string]$Message, - [string]$DestFile - ) - $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - $line = "[$ts] $Message" - Write-Host $line - if ($DestFile) { - $dir = Split-Path -Parent $DestFile - if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } - Add-Content -Path $DestFile -Value $line - } -} - -function Ensure-Workspace { - if (!(Test-Path $GLOBAL_DIR)) { - New-Item -ItemType Directory -Path $GLOBAL_DIR | Out-Null - Log-Line "✅ Created Onboarding workspace: $GLOBAL_DIR" $GLOBAL_LOG - } else { - Log-Line "ℹ️ Onboarding Workspace already exists: $GLOBAL_DIR" $GLOBAL_LOG - } -} - -function Invoke-GitClone { - param( - [Parameter(Mandatory)] [string]$Url, - [Parameter(Mandatory)] [string]$Target, - [string]$Branch, - [string]$LogFile - ) - $args = @("clone") - if ($Branch) { $args += @("-b", $Branch) } - $args += @($Url, $Target) - - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = "git" - $psi.Arguments = ($args | ForEach-Object { - if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } - }) -join ' ' - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - $psi.WorkingDirectory = (Get-Location).Path - - $p = New-Object System.Diagnostics.Process - $p.StartInfo = $psi - [void]$p.Start() - $stdout = $p.StandardOutput.ReadToEnd() - $stderr = $p.StandardError.ReadToEnd() - $p.WaitForExit() - - if ($LogFile) { - if ($stdout) { Add-Content -Path $LogFile -Value $stdout } - if ($stderr) { Add-Content -Path $LogFile -Value $stderr } - } - - if ($p.ExitCode -ne 0) { - throw "git clone failed (exit $($p.ExitCode)): $stderr" - } -} - -function Set-ContentNoBom { - param( - [Parameter(Mandatory)][string]$Path, - [Parameter(Mandatory)][string]$Value - ) - $enc = New-Object System.Text.UTF8Encoding($false) # no BOM - [System.IO.File]::WriteAllText($Path, $Value, $enc) -} - -# Run external tools capturing stdout/stderr without throwing on STDERR -function Invoke-External { - param( - [Parameter(Mandatory)][string]$Exe, - [Parameter()][string[]]$Arguments = @(), - [string]$LogFile, - [string]$WorkingDirectory - ) - $psi = New-Object System.Diagnostics.ProcessStartInfo - $exeToRun = $Exe - $argLine = ($Arguments | ForEach-Object { if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } }) -join ' ' - - # .cmd/.bat need to be invoked via cmd.exe when UseShellExecute=false - $ext = [System.IO.Path]::GetExtension($Exe) - if ($ext -and ($ext.ToLowerInvariant() -in @('.cmd','.bat'))) { - if (-not (Test-Path $Exe)) { throw "Command not found: $Exe" } - $psi.FileName = "cmd.exe" - $psi.Arguments = "/c `"$Exe`" $argLine" - } else { - $psi.FileName = $exeToRun - $psi.Arguments = $argLine - } - - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) { - $psi.WorkingDirectory = (Get-Location).Path - } else { - $psi.WorkingDirectory = $WorkingDirectory - } - - $p = New-Object System.Diagnostics.Process - $p.StartInfo = $psi - - # Stream output to log file in real-time if LogFile is specified - if ($LogFile) { - # Ensure the log file directory exists - $logDir = Split-Path -Parent $LogFile - if ($logDir -and !(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null } - - # Create script blocks to handle output streaming - $stdoutAction = { - if (-not [string]::IsNullOrEmpty($EventArgs.Data)) { - Add-Content -Path $Event.MessageData -Value $EventArgs.Data - } - } - $stderrAction = { - if (-not [string]::IsNullOrEmpty($EventArgs.Data)) { - Add-Content -Path $Event.MessageData -Value $EventArgs.Data - } - } - - # Register events to capture output line by line as it's produced - $stdoutEvent = Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $stdoutAction -MessageData $LogFile - $stderrEvent = Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $stderrAction -MessageData $LogFile - - [void]$p.Start() - $p.BeginOutputReadLine() - $p.BeginErrorReadLine() - $p.WaitForExit() - - # Clean up event handlers - Unregister-Event -SourceIdentifier $stdoutEvent.Name - Unregister-Event -SourceIdentifier $stderrEvent.Name - Remove-Job -Id $stdoutEvent.Id -Force - Remove-Job -Id $stderrEvent.Id -Force - } else { - # If no log file, just read all output at once (original behavior) - [void]$p.Start() - $stdout = $p.StandardOutput.ReadToEnd() - $stderr = $p.StandardError.ReadToEnd() - $p.WaitForExit() - } - - return $p.ExitCode -} - -# Return a Maven executable path or wrapper for a given repo directory -function Get-MavenCommand { - param([Parameter(Mandatory)][string]$RepoDir) - $mvnCmd = Get-Command mvn -ErrorAction SilentlyContinue - if ($mvnCmd) { return $mvnCmd.Source } - $wrapper = Join-Path $RepoDir "mvnw.cmd" - if (Test-Path $wrapper) { return $wrapper } - throw "Maven not found in PATH and 'mvnw.cmd' not present under $RepoDir. Install Maven or ensure the wrapper exists." -} - -# Get the python.exe inside a Windows venv -function Get-VenvPython { - param([Parameter(Mandatory)][string]$VenvDir) - $py = Join-Path $VenvDir "Scripts\python.exe" - if (Test-Path $py) { return $py } - throw "Python interpreter not found in venv: $VenvDir" -} - -# Detect a working Python interpreter and set $PY_CMD accordingly -function Set-PythonCmd { - $candidates = @( - @("python3"), - @("python"), - @("py","-3"), - @("py") - ) - foreach ($cand in $candidates) { - try { - $exe = $cand[0] - $args = @() - if ($cand.Length -gt 1) { $args = $cand[1..($cand.Length-1)] } - $code = Invoke-External -Exe $exe -Arguments ($args + @("--version")) -LogFile $null - if ($code -eq 0) { - $script:PY_CMD = $cand - return - } - } catch {} - } - throw "Python not found via python3/python/py. Please install Python 3 and ensure it's on PATH." -} - -# Invoke Python with arguments using the detected interpreter -function Invoke-Py { - param( - [Parameter(Mandatory)][string[]]$Arguments, - [string]$LogFile, - [string]$WorkingDirectory - ) - if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } - $exe = $PY_CMD[0] - $baseArgs = @() - if ($PY_CMD.Count -gt 1) { $baseArgs = $PY_CMD[1..($PY_CMD.Count-1)] } - return (Invoke-External -Exe $exe -Arguments ($baseArgs + $Arguments) -LogFile $LogFile -WorkingDirectory $WorkingDirectory) -} - -# Spinner function for long-running operations -function Show-Spinner { - param([Parameter(Mandatory)][System.Diagnostics.Process]$Process) - $spin = @('|','/','-','\') - $i = 0 - $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - while (!$Process.HasExited) { - Write-Host "`r[$ts] ⏳ Processing... $($spin[$i])" -NoNewline - $i = ($i + 1) % 4 - Start-Sleep -Milliseconds 100 - } - Write-Host "`r[$ts] ✅ Done! " -} - -# Check if IP is private -function Test-PrivateIP { - param([string]$IP) - # If IP resolution failed (empty), assume it's a public domain - # BrowserStack Local should only be enabled for confirmed private IPs - if ([string]::IsNullOrWhiteSpace($IP)) { return $false } - $parts = $IP.Split('.') - if ($parts.Count -ne 4) { return $false } - $first = [int]$parts[0] - $second = [int]$parts[1] - if ($first -eq 10) { return $true } - if ($first -eq 192 -and $second -eq 168) { return $true } - if ($first -eq 172 -and $second -ge 16 -and $second -le 31) { return $true } - return $false -} - -# Check if domain is private -function Test-DomainPrivate { - $domain = $CX_TEST_URL -replace '^https?://', '' -replace '/.*$', '' - Log-Line "Website domain: $domain" $GLOBAL_LOG - $env:NOW_WEB_DOMAIN = $CX_TEST_URL - - # Resolve domain using Resolve-DnsName (more reliable than nslookup) - $IP_ADDRESS = "" - try { - # Try using Resolve-DnsName first (Windows PowerShell 5.1+) - $dnsResult = Resolve-DnsName -Name $domain -Type A -ErrorAction Stop | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1 - if ($dnsResult) { - $IP_ADDRESS = $dnsResult.IPAddress - } - } catch { - # Fallback to nslookup if Resolve-DnsName fails - try { - $nslookupOutput = nslookup $domain 2>&1 | Out-String - # Extract IP addresses from nslookup output (match IPv4 pattern) - if ($nslookupOutput -match '(?:Address|Addresses):\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})') { - $IP_ADDRESS = $matches[1] - } - } catch { - Log-Line "⚠️ Failed to resolve domain: $domain (assuming public domain)" $GLOBAL_LOG - $IP_ADDRESS = "" - } - } - - if ([string]::IsNullOrWhiteSpace($IP_ADDRESS)) { - Log-Line "⚠️ DNS resolution failed for: $domain (treating as public domain, BrowserStack Local will be DISABLED)" $GLOBAL_LOG - } else { - Log-Line "✅ Resolved IP: $IP_ADDRESS" $GLOBAL_LOG - } - - return (Test-PrivateIP -IP $IP_ADDRESS) -} - -# ===== GUI helpers ===== -function Show-InputBox { - param( - [string]$Title = "Input", - [string]$Prompt = "Enter value:", - [string]$DefaultText = "" - ) - $form = New-Object System.Windows.Forms.Form - $form.Text = $Title - $form.Size = New-Object System.Drawing.Size(500,220) - $form.StartPosition = "CenterScreen" - - $label = New-Object System.Windows.Forms.Label - $label.Text = $Prompt - $label.MaximumSize = New-Object System.Drawing.Size(460,0) - $label.AutoSize = $true - $label.Location = New-Object System.Drawing.Point(10,20) - $form.Controls.Add($label) - - $textBox = New-Object System.Windows.Forms.TextBox - $textBox.Size = New-Object System.Drawing.Size(460,20) - $textBox.Location = New-Object System.Drawing.Point(10,($label.Bottom + 10)) - $textBox.Text = $DefaultText - $form.Controls.Add($textBox) - - $okButton = New-Object System.Windows.Forms.Button - $okButton.Text = "OK" - $okButton.Location = New-Object System.Drawing.Point(380,($textBox.Bottom + 20)) - $okButton.Add_Click({ $form.Tag = $textBox.Text; $form.Close() }) - $form.Controls.Add($okButton) - - $form.AcceptButton = $okButton - [void]$form.ShowDialog() - return [string]$form.Tag -} - -function Show-PasswordBox { - param( - [string]$Title = "Secret", - [string]$Prompt = "Enter secret:" - ) - $form = New-Object System.Windows.Forms.Form - $form.Text = $Title - $form.Size = New-Object System.Drawing.Size(500,220) - $form.StartPosition = "CenterScreen" - - $label = New-Object System.Windows.Forms.Label - $label.Text = $Prompt - $label.MaximumSize = New-Object System.Drawing.Size(460,0) - $label.AutoSize = $true - $label.Location = New-Object System.Drawing.Point(10,20) - $form.Controls.Add($label) - - $textBox = New-Object System.Windows.Forms.TextBox - $textBox.Size = New-Object System.Drawing.Size(460,20) - $textBox.Location = New-Object System.Drawing.Point(10,($label.Bottom + 10)) - $textBox.UseSystemPasswordChar = $true - $form.Controls.Add($textBox) +# ===== Import utilities (like Mac's source commands) ===== +$script:PSScriptRootResolved = Split-Path -Parent $MyInvocation.MyCommand.Path +. (Join-Path $PSScriptRootResolved "logging-utils.ps1") +. (Join-Path $PSScriptRootResolved "common-utils.ps1") +. (Join-Path $PSScriptRootResolved "device-machine-allocation.ps1") +. (Join-Path $PSScriptRootResolved "user-interaction.ps1") +. (Join-Path $PSScriptRootResolved "env-prequisite-checks.ps1") +. (Join-Path $PSScriptRootResolved "env-setup-run.ps1") - $okButton = New-Object System.Windows.Forms.Button - $okButton.Text = "OK" - $okButton.Location = New-Object System.Drawing.Point(380,($textBox.Bottom + 20)) - $okButton.Add_Click({ $form.Tag = $textBox.Text; $form.Close() }) - $form.Controls.Add($okButton) - - $form.AcceptButton = $okButton - [void]$form.ShowDialog() - return [string]$form.Tag -} - -function Show-ChoiceBox { - param( - [string]$Title = "Choose", - [string]$Prompt = "Select one:", - [string[]]$Choices, - [string]$DefaultChoice - ) - $form = New-Object System.Windows.Forms.Form - $form.Text = $Title - $form.Size = New-Object System.Drawing.Size(420, 240) - $form.StartPosition = "CenterScreen" - - $label = New-Object System.Windows.Forms.Label - $label.Text = $Prompt - $label.AutoSize = $true - $label.Location = New-Object System.Drawing.Point(10, 10) - $form.Controls.Add($label) - - $group = New-Object System.Windows.Forms.Panel - $group.Location = New-Object System.Drawing.Point(10, 35) - $group.Width = 380 - $startY = 10 - $spacing = 28 - - $radios = @() - [int]$i = 0 - foreach ($c in $Choices) { - $rb = New-Object System.Windows.Forms.RadioButton - $rb.Text = $c - $rb.AutoSize = $true - $rb.Location = New-Object System.Drawing.Point(10, ($startY + $i * $spacing)) - if ($c -eq $DefaultChoice) { $rb.Checked = $true } - $group.Controls.Add($rb) - $radios += $rb - $i++ - } - $group.Height = [Math]::Max(120, $startY + ($Choices.Count * $spacing) + 10) - $form.Controls.Add($group) - - $ok = New-Object System.Windows.Forms.Button - $ok.Text = "OK" - $ok.Location = New-Object System.Drawing.Point(300, ($group.Bottom + 10)) - $ok.Add_Click({ - foreach ($rb in $radios) { if ($rb.Checked) { $form.Tag = $rb.Text; break } } - $form.Close() - }) - $form.Controls.Add($ok) - - $form.Height = $ok.Bottom + 70 - $form.AcceptButton = $ok - [void]$form.ShowDialog() - return [string]$form.Tag -} - -# === NEW: Big clickable button chooser === -function Show-ClickChoice { - param( - [string]$Title = "Choose", - [string]$Prompt = "Select one:", - [string[]]$Choices, - [string]$DefaultChoice - ) - if (-not $Choices -or $Choices.Count -eq 0) { return "" } - - $form = New-Object System.Windows.Forms.Form - $form.Text = $Title - $form.StartPosition = "CenterScreen" - $form.MinimizeBox = $false - $form.MaximizeBox = $false - $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog - $form.BackColor = [System.Drawing.Color]::FromArgb(245,245,245) - - $label = New-Object System.Windows.Forms.Label - $label.Text = $Prompt - $label.AutoSize = $true - $label.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Regular) - $label.Location = New-Object System.Drawing.Point(12, 12) - $form.Controls.Add($label) - - $panel = New-Object System.Windows.Forms.FlowLayoutPanel - $panel.Location = New-Object System.Drawing.Point(12, 40) - $panel.Size = New-Object System.Drawing.Size(460, 140) - $panel.WrapContents = $true - $panel.AutoScroll = $true - $panel.FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight - $form.Controls.Add($panel) - - $selected = $null - foreach ($c in $Choices) { - $btn = New-Object System.Windows.Forms.Button - $btn.Text = $c - $btn.Width = 140 - $btn.Height = 40 - $btn.Margin = '8,8,8,8' - $btn.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) - $btn.FlatStyle = 'System' - if ($c -eq $DefaultChoice) { - $btn.BackColor = [System.Drawing.Color]::FromArgb(232,240,254) - } - $btn.Add_Click({ - $script:selected = $this.Text - $form.Tag = $script:selected - $form.Close() - }) - $panel.Controls.Add($btn) - } - - $cancel = New-Object System.Windows.Forms.Button - $cancel.Text = "Cancel" - $cancel.Width = 90 - $cancel.Height = 32 - $cancel.Location = New-Object System.Drawing.Point(382, 188) - $cancel.Add_Click({ $form.Tag = ""; $form.Close() }) - $form.Controls.Add($cancel) - $form.CancelButton = $cancel - - $form.ClientSize = New-Object System.Drawing.Size(484, 230) - [void]$form.ShowDialog() - return [string]$form.Tag -} - -function Show-OpenFileDialog { - param( - [string]$Title = "Select File", - [string]$Filter = "All files (*.*)|*.*" - ) - $ofd = New-Object System.Windows.Forms.OpenFileDialog - $ofd.Title = $Title - $ofd.Filter = $Filter - $ofd.Multiselect = $false - if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { - return $ofd.FileName - } - return "" -} - -# ===== Baseline interactions ===== -function Ask-BrowserStack-Credentials { - $script:BROWSERSTACK_USERNAME = Show-InputBox -Title "BrowserStack Setup" -Prompt "Enter your BrowserStack Username:`n`nNote: Locate it in your BrowserStack account page`nhttps://www.browserstack.com/accounts/profile/details" -DefaultText "" - if ([string]::IsNullOrWhiteSpace($script:BROWSERSTACK_USERNAME)) { - Log-Line "❌ Username empty" $GLOBAL_LOG - throw "Username is required" - } - $script:BROWSERSTACK_ACCESS_KEY = Show-PasswordBox -Title "BrowserStack Setup" -Prompt "Enter your BrowserStack Access Key:`n`nNote: Locate it in your BrowserStack account page`nhttps://www.browserstack.com/accounts/profile/details" - if ([string]::IsNullOrWhiteSpace($script:BROWSERSTACK_ACCESS_KEY)) { - Log-Line "❌ Access Key empty" $GLOBAL_LOG - throw "Access Key is required" - } - Log-Line "✅ BrowserStack credentials captured (access key hidden)" $GLOBAL_LOG -} - -# === UPDATED: click-select for Web/App/Both === -function Ask-Test-Type { - $choice = Show-ClickChoice -Title "Testing Type" ` - -Prompt "What do you want to run?" ` - -Choices @("Web","App","Both") ` - -DefaultChoice "Web" - if ([string]::IsNullOrWhiteSpace($choice)) { throw "No testing type selected" } - $script:TEST_TYPE = $choice - Log-Line "✅ Selected Testing Type: $script:TEST_TYPE" $GLOBAL_LOG - - switch ($script:TEST_TYPE) { - "Web" { Ask-User-TestUrl } - "App" { Ask-And-Upload-App } - "Both" { Ask-User-TestUrl; Ask-And-Upload-App } - } -} - -# === UPDATED: click-select for Tech Stack === -function Ask-Tech-Stack { - $choice = Show-ClickChoice -Title "Tech Stack" ` - -Prompt "Select your installed language / framework:" ` - -Choices @("Java","Python","NodeJS") ` - -DefaultChoice "Java" - if ([string]::IsNullOrWhiteSpace($choice)) { throw "No tech stack selected" } - $script:TECH_STACK = $choice - Log-Line "✅ Selected Tech Stack: $script:TECH_STACK" $GLOBAL_LOG -} - -function Validate-Tech-Stack { - Log-Line "ℹ️ Checking prerequisites for $script:TECH_STACK" $GLOBAL_LOG - switch ($script:TECH_STACK) { - "Java" { - Log-Line "🔍 Checking if 'java' command exists..." $GLOBAL_LOG - if (-not (Get-Command java -ErrorAction SilentlyContinue)) { - Log-Line "❌ Java command not found in PATH." $GLOBAL_LOG - throw "Java not found" - } - Log-Line "🔍 Checking if Java runs correctly..." $GLOBAL_LOG - $verInfo = & cmd /c 'java -version 2>&1' - if (-not $verInfo) { - Log-Line "❌ Java exists but failed to run." $GLOBAL_LOG - throw "Java invocation failed" - } - Log-Line "✅ Java is installed. Version details:" $GLOBAL_LOG - ($verInfo -split "`r?`n") | ForEach-Object { if ($_ -ne "") { Log-Line " $_" $GLOBAL_LOG } } - } - "Python" { - Log-Line "🔍 Checking if 'python3' command exists..." $GLOBAL_LOG - try { - Set-PythonCmd - Log-Line "🔍 Checking if Python3 runs correctly..." $GLOBAL_LOG - $code = Invoke-Py -Arguments @("--version") -LogFile $null -WorkingDirectory (Get-Location).Path - if ($code -eq 0) { - Log-Line ("✅ Python3 is installed: {0}" -f ( ($PY_CMD -join ' ') )) $GLOBAL_LOG - } else { - throw "Python present but failed to execute" - } - } catch { - Log-Line "❌ Python3 exists but failed to run." $GLOBAL_LOG - throw - } - } - - "NodeJS" { - Log-Line "🔍 Checking if 'node' command exists..." $GLOBAL_LOG - if (-not (Get-Command node -ErrorAction SilentlyContinue)) { - Log-Line "❌ Node.js command not found in PATH." $GLOBAL_LOG - throw "Node not found" - } - Log-Line "🔍 Checking if 'npm' command exists..." $GLOBAL_LOG - if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { - Log-Line "❌ npm command not found in PATH." $GLOBAL_LOG - throw "npm not found" - } - Log-Line "🔍 Checking if Node.js runs correctly..." $GLOBAL_LOG - $nodeVer = & node -v 2>&1 - if (-not $nodeVer) { - Log-Line "❌ Node.js exists but failed to run." $GLOBAL_LOG - throw "Node.js invocation failed" - } - Log-Line "🔍 Checking if npm runs correctly..." $GLOBAL_LOG - $npmVer = & npm -v 2>&1 - if (-not $npmVer) { - Log-Line "❌ npm exists but failed to run." $GLOBAL_LOG - throw "npm invocation failed" - } - Log-Line "✅ Node.js is installed: $nodeVer" $GLOBAL_LOG - Log-Line "✅ npm is installed: $npmVer" $GLOBAL_LOG - } - default { Log-Line "❌ Unknown tech stack selected: $script:TECH_STACK" $GLOBAL_LOG; throw "Unknown tech stack" } - } - Log-Line "✅ Prerequisites validated for $script:TECH_STACK" $GLOBAL_LOG -} -# fix Python branch without ternary -function Get-PythonCmd { - if (Get-Command python3 -ErrorAction SilentlyContinue) { return "python3" } - return "python" -} - -function Ask-User-TestUrl { - $u = Show-InputBox -Title "Test URL Setup" -Prompt "Enter the URL you want to test with BrowserStack:`n(Leave blank for default: $DEFAULT_TEST_URL)" -DefaultText "" - if ([string]::IsNullOrWhiteSpace($u)) { - $script:CX_TEST_URL = $DEFAULT_TEST_URL - Log-Line "⚠️ No URL entered. Falling back to default: $script:CX_TEST_URL" $GLOBAL_LOG +# ===== Main flow (baseline steps then run) ===== +try { + # Get test type and tech stack before logging + if ($RunMode -match "--silent|--debug") { + $textInfo = (Get-Culture).TextInfo + $ttCandidate = if ($TT) { $TT } else { $env:TEST_TYPE } + if ([string]::IsNullOrWhiteSpace($ttCandidate)) { throw "TEST_TYPE is required in silent/debug mode." } + $tsCandidate = if ($TSTACK) { $TSTACK } else { $env:TECH_STACK } + if ([string]::IsNullOrWhiteSpace($tsCandidate)) { throw "TECH_STACK is required in silent/debug mode." } + $script:TEST_TYPE = $textInfo.ToTitleCase($ttCandidate.ToLowerInvariant()) + $script:TECH_STACK = $textInfo.ToTitleCase($tsCandidate.ToLowerInvariant()) + if ($TEST_TYPE -notin @("Web","App")) { throw "TEST_TYPE must be either 'Web' or 'App'." } + if ($TECH_STACK -notin @("Java","Python","NodeJS")) { throw "TECH_STACK must be one of: Java, Python, NodeJS." } } else { - $script:CX_TEST_URL = $u - Log-Line "🌐 Using custom test URL: $script:CX_TEST_URL" $GLOBAL_LOG - } -} - -function Get-BasicAuthHeader { - param([string]$User, [string]$Key) - $pair = "{0}:{1}" -f $User,$Key - $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair) - "Basic {0}" -f [System.Convert]::ToBase64String($bytes) -} - -function Ask-And-Upload-App { - # First, show a choice screen for Sample App vs Browse - $appChoice = Show-ClickChoice -Title "App Selection" ` - -Prompt "Choose an app to test:" ` - -Choices @("Sample App","Browse") ` - -DefaultChoice "Sample App" - - if ([string]::IsNullOrWhiteSpace($appChoice) -or $appChoice -eq "Sample App") { - Log-Line "⚠️ Using default sample app: bs://sample.app" $GLOBAL_LOG - $script:APP_URL = "bs://sample.app" - $script:APP_PLATFORM = "all" - return - } - - # User chose "Browse", so open file picker - $path = Show-OpenFileDialog -Title "📱 Select your .apk or .ipa file" -Filter "App Files (*.apk;*.ipa)|*.apk;*.ipa|All files (*.*)|*.*" - if ([string]::IsNullOrWhiteSpace($path)) { - Log-Line "⚠️ No app selected. Using default sample app: bs://sample.app" $GLOBAL_LOG - $script:APP_URL = "bs://sample.app" - $script:APP_PLATFORM = "all" - return - } - - $ext = [System.IO.Path]::GetExtension($path).ToLowerInvariant() - switch ($ext) { - ".apk" { $script:APP_PLATFORM = "android" } - ".ipa" { $script:APP_PLATFORM = "ios" } - default { Log-Line "❌ Unsupported file type. Only .apk or .ipa allowed." $GLOBAL_LOG; throw "Unsupported app file" } - } - - Log-Line "⬆️ Uploading $path to BrowserStack..." $GLOBAL_LOG - - # Create multipart form data manually for PowerShell 5.1 compatibility - $boundary = [System.Guid]::NewGuid().ToString() - $LF = "`r`n" - $fileBin = [System.IO.File]::ReadAllBytes($path) - $fileName = [System.IO.Path]::GetFileName($path) - - $bodyLines = ( - "--$boundary", - "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", - "Content-Type: application/octet-stream$LF", - [System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileBin), - "--$boundary--$LF" - ) -join $LF - - $headers = @{ - Authorization = (Get-BasicAuthHeader -User $BROWSERSTACK_USERNAME -Key $BROWSERSTACK_ACCESS_KEY) - "Content-Type" = "multipart/form-data; boundary=$boundary" - } - - $resp = Invoke-RestMethod -Method Post -Uri "https://api-cloud.browserstack.com/app-automate/upload" -Headers $headers -Body $bodyLines - $url = $resp.app_url - if ([string]::IsNullOrWhiteSpace($url)) { - Log-Line "❌ Upload failed. Response: $(ConvertTo-Json $resp -Depth 5)" $GLOBAL_LOG - throw "Upload failed" - } - $script:APP_URL = $url - Log-Line "✅ App uploaded successfully: $script:APP_URL" $GLOBAL_LOG -} - -# ===== Generators ===== -function Generate-Web-Platforms-Yaml { - param([int]$MaxTotalParallels) - $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) - if ($max -lt 0) { $max = 0 } - $sb = New-Object System.Text.StringBuilder - $count = 0 - - foreach ($t in $WEB_PLATFORM_TEMPLATES) { - $parts = $t.Split('|') - $os = $parts[0]; $osVersion = $parts[1]; $browserName = $parts[2] - foreach ($version in @('latest','latest-1','latest-2')) { - [void]$sb.AppendLine(" - os: $os") - [void]$sb.AppendLine(" osVersion: $osVersion") - [void]$sb.AppendLine(" browserName: $browserName") - [void]$sb.AppendLine(" browserVersion: $version") - $count++ - if ($count -ge $max -and $max -gt 0) { - return $sb.ToString() - } - } - } - return $sb.ToString() -} - -function Generate-Mobile-Platforms-Yaml { - param([int]$MaxTotalParallels) - $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) - if ($max -lt 1) { $max = 1 } - $sb = New-Object System.Text.StringBuilder - $count = 0 - - foreach ($t in $MOBILE_ALL) { - $parts = $t.Split('|') - $platformName = $parts[0] - $deviceName = $parts[1] - $platformVer = $parts[2] - - if (-not [string]::IsNullOrWhiteSpace($APP_PLATFORM)) { - if ($APP_PLATFORM -eq 'ios' -and $platformName -ne 'ios') { continue } - if ($APP_PLATFORM -eq 'android' -and $platformName -ne 'android') { continue } - } - - [void]$sb.AppendLine(" - platformName: $platformName") - [void]$sb.AppendLine(" deviceName: $deviceName") - [void]$sb.AppendLine(" platformVersion: '${platformVer}.0'") - $count++ - if ($count -ge $max) { return $sb.ToString() } - } - return $sb.ToString() -} - -function Generate-Mobile-Caps-Json { - param([int]$MaxTotalParallels, [string]$OutputFile) - $max = $MaxTotalParallels - if ($max -lt 1) { $max = 1 } - - $items = @() - $count = 0 - - foreach ($t in $MOBILE_ALL) { - $parts = $t.Split('|') - $platformName = $parts[0] - $deviceName = $parts[1] - $platformVer = $parts[2] - - # Filter based on APP_PLATFORM - if (-not [string]::IsNullOrWhiteSpace($APP_PLATFORM)) { - if ($APP_PLATFORM -eq 'ios' -and $platformName -ne 'ios') { continue } - if ($APP_PLATFORM -eq 'android' -and $platformName -ne 'android') { continue } - # If APP_PLATFORM is 'all', include both ios and android (no filtering) - } - - $items += [pscustomobject]@{ - 'bstack:options' = @{ - deviceName = $deviceName - osVersion = "${platformVer}.0" - } - } - $count++ - if ($count -ge $max) { break } - } - - # Convert to JSON - $json = ($items | ConvertTo-Json -Depth 5) - - # Write to file - Set-ContentNoBom -Path $OutputFile -Value $json - - return $json -} - -function Generate-Web-Caps-Json { - param([int]$MaxTotalParallels) - $max = [Math]::Floor($MaxTotalParallels * $PARALLEL_PERCENTAGE) - if ($max -lt 1) { $max = 1 } - - $items = @() - $count = 0 - foreach ($t in $WEB_PLATFORM_TEMPLATES) { - $parts = $t.Split('|') - $os = $parts[0]; $osVersion = $parts[1]; $browserName = $parts[2] - foreach ($version in @('latest','latest-1','latest-2')) { - $items += [pscustomobject]@{ - browserName = $browserName - browserVersion= $version - 'bstack:options' = @{ - os = $os - osVersion = $osVersion - } - } - $count++ - if ($count -ge $max) { break } - } - if ($count -ge $max) { break } - } - - # Convert to JSON and remove outer brackets to match macOS behavior - # The test code adds brackets: JSON.parse("[" + process.env.BSTACK_CAPS_JSON + "]") - $json = ($items | ConvertTo-Json -Depth 5) - - # Remove leading [ and trailing ] - if ($json.StartsWith('[')) { - $json = $json.Substring(1) - } - if ($json.EndsWith(']')) { - $json = $json.Substring(0, $json.Length - 1) - } - - # Trim any leading/trailing whitespace - $json = $json.Trim() - - return $json -} - -# ===== Fetch plan details ===== -function Fetch-Plan-Details { - Log-Line "ℹ️ Fetching BrowserStack Plan Details..." $GLOBAL_LOG - $auth = Get-BasicAuthHeader -User $BROWSERSTACK_USERNAME -Key $BROWSERSTACK_ACCESS_KEY - $headers = @{ Authorization = $auth } - - if ($TEST_TYPE -in @("Web","Both")) { - try { - $resp = Invoke-RestMethod -Method Get -Uri "https://api.browserstack.com/automate/plan.json" -Headers $headers - $script:WEB_PLAN_FETCHED = $true - $script:TEAM_PARALLELS_MAX_ALLOWED_WEB = [int]$resp.parallel_sessions_max_allowed - Log-Line "✅ Web Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_WEB" $GLOBAL_LOG - } catch { - Log-Line "❌ Web Testing Plan fetch failed ($($_.Exception.Message))" $GLOBAL_LOG - } - } - if ($TEST_TYPE -in @("App","Both")) { - try { - $resp2 = Invoke-RestMethod -Method Get -Uri "https://api-cloud.browserstack.com/app-automate/plan.json" -Headers $headers - $script:MOBILE_PLAN_FETCHED = $true - $script:TEAM_PARALLELS_MAX_ALLOWED_MOBILE = [int]$resp2.parallel_sessions_max_allowed - Log-Line "✅ Mobile App Testing Plan fetched: Team max parallel sessions = $TEAM_PARALLELS_MAX_ALLOWED_MOBILE" $GLOBAL_LOG - } catch { - Log-Line "❌ Mobile App Testing Plan fetch failed ($($_.Exception.Message))" $GLOBAL_LOG - } - } - - if ( ($TEST_TYPE -eq "Web" -and -not $WEB_PLAN_FETCHED) -or - ($TEST_TYPE -eq "App" -and -not $MOBILE_PLAN_FETCHED) -or - ($TEST_TYPE -eq "Both" -and -not ($WEB_PLAN_FETCHED -or $MOBILE_PLAN_FETCHED)) ) { - Log-Line "❌ Unauthorized to fetch required plan(s) or failed request(s). Exiting." $GLOBAL_LOG - throw "Plan fetch failed" - } -} - -# ===== Setup: Web (Java) ===== -function Setup-Web-Java { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - $REPO = "now-testng-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - Log-Line "📦 Cloning repo $REPO into $TARGET" $GLOBAL_LOG - Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile $WEB_LOG - - Push-Location $TARGET - try { - # Check if domain is private - if (Test-DomainPrivate) { - $UseLocal = $true - } - - # Log local flag status - if ($UseLocal) { - Log-Line "✅ BrowserStack Local is ENABLED for this run." $GLOBAL_LOG - } else { - Log-Line "✅ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG - } - - # Generate YAML config in the correct location - Log-Line "🧩 Generating YAML config (browserstack.yml)" $GLOBAL_LOG - $platforms = Generate-Web-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_WEB - $localFlag = if ($UseLocal) { "true" } else { "false" } - - $yamlContent = @" -userName: $BROWSERSTACK_USERNAME -accessKey: $BROWSERSTACK_ACCESS_KEY -framework: testng -browserstackLocal: $localFlag -buildName: now-testng-java-web -projectName: NOW-Web-Test -percy: true -accessibility: true -platforms: -$platforms -parallelsPerPlatform: $ParallelsPerPlatform -"@ - - Set-Content "browserstack.yml" -Value $yamlContent - Log-Line "✅ Created browserstack.yml in root directory" $GLOBAL_LOG - - $mvn = Get-MavenCommand -RepoDir $TARGET - Log-Line "⚙️ Running '$mvn compile'" $GLOBAL_LOG - [void](Invoke-External -Exe $mvn -Arguments @("compile") -LogFile $LogFile -WorkingDirectory $TARGET) - - Log-Line "🚀 Running '$mvn test -P sample-test'. This could take a few minutes. Follow the Automation build here: https://automation.browserstack.com/" $GLOBAL_LOG - [void](Invoke-External -Exe $mvn -Arguments @("test","-P","sample-test") -LogFile $LogFile -WorkingDirectory $TARGET) - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Setup: Web (Python) ===== -function Setup-Web-Python { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - $REPO = "now-pytest-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile $WEB_LOG - Log-Line "✅ Cloned repository: $REPO into $TARGET" $GLOBAL_LOG - - Push-Location $TARGET - try { - if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } - $venv = Join-Path $TARGET "venv" - if (!(Test-Path $venv)) { - [void](Invoke-Py -Arguments @("-m","venv",$venv) -LogFile $LogFile -WorkingDirectory $TARGET) - Log-Line "✅ Created Python virtual environment" $GLOBAL_LOG - } - $venvPy = Get-VenvPython -VenvDir $venv - [void](Invoke-External -Exe $venvPy -Arguments @("-m","pip","install","-r","requirements.txt") -LogFile $LogFile -WorkingDirectory $TARGET) - # Ensure SDK can find pytest on PATH - $env:PATH = (Join-Path $venv 'Scripts') + ";" + $env:PATH - - $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME - $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY - - # Check if domain is private - if (Test-DomainPrivate) { - $UseLocal = $true - } - - # Log local flag status - if ($UseLocal) { - Log-Line "✅ BrowserStack Local is ENABLED for this run." $GLOBAL_LOG - } else { - Log-Line "✅ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG - } - - $env:BROWSERSTACK_CONFIG_FILE = "browserstack.yml" - $platforms = Generate-Web-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_WEB - $localFlag = if ($UseLocal) { "true" } else { "false" } - -@" -userName: $BROWSERSTACK_USERNAME -accessKey: $BROWSERSTACK_ACCESS_KEY -framework: pytest -browserstackLocal: $localFlag -buildName: browserstack-sample-python-web -projectName: NOW-Web-Test -percy: true -accessibility: true -platforms: -$platforms -parallelsPerPlatform: $ParallelsPerPlatform -"@ | Set-Content "browserstack.yml" - - Log-Line "✅ Updated root-level browserstack.yml with platforms and credentials" $GLOBAL_LOG - - $sdk = Join-Path $venv "Scripts\browserstack-sdk.exe" - Log-Line "🚀 Running 'browserstack-sdk pytest -s tests/bstack-sample-test.py'. This could take a few minutes. Follow the Automation build here: https://automation.browserstack.com/" $GLOBAL_LOG - [void](Invoke-External -Exe $sdk -Arguments @('pytest','-s','tests/bstack-sample-test.py') -LogFile $LogFile -WorkingDirectory $TARGET) - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Setup: Web (NodeJS) ===== -function Setup-Web-NodeJS { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - $REPO = "now-webdriverio-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - - Log-Line "📦 Cloning repo $REPO into $TARGET" $GLOBAL_LOG - Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile $WEB_LOG - - Push-Location $TARGET - try { - Log-Line "⚙️ Running 'npm install'" $GLOBAL_LOG - [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","install") -LogFile $LogFile -WorkingDirectory $TARGET) - - # Generate capabilities JSON - Log-Line "🧩 Generating browser/OS capabilities" $GLOBAL_LOG - $caps = Generate-Web-Caps-Json -MaxTotalParallels $ParallelsPerPlatform - - $env:BSTACK_PARALLELS = $ParallelsPerPlatform - $env:BSTACK_CAPS_JSON = $caps - - # Check if domain is private - if (Test-DomainPrivate) { - $UseLocal = $true - } - - # Log local flag status - if ($UseLocal) { - Log-Line "✅ BrowserStack Local is ENABLED for this run." $GLOBAL_LOG - } else { - Log-Line "✅ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG - } - - $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME - $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY - $localFlagStr = if ($UseLocal) { "true" } else { "false" } - $env:BROWSERSTACK_LOCAL = $localFlagStr - $env:BSTACK_PARALLELS = $ParallelsPerPlatform - - Log-Line "🚀 Running 'npm run test'" $GLOBAL_LOG - [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","run","test") -LogFile $LogFile -WorkingDirectory $TARGET) - - Log-Line "✅ Web NodeJS setup and test execution completed successfully." $GLOBAL_LOG - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Setup: Mobile (Python) ===== -function Setup-Mobile-Python { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - $REPO = "pytest-appium-app-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - Invoke-GitClone -Url "https://github.com/browserstack/$REPO.git" -Target $TARGET -LogFile $MOBILE_LOG - Log-Line "✅ Cloned repository: $REPO into $TARGET" $GLOBAL_LOG - - Push-Location $TARGET - try { - if (-not $PY_CMD -or $PY_CMD.Count -eq 0) { Set-PythonCmd } - $venv = Join-Path $TARGET "venv" - if (!(Test-Path $venv)) { - [void](Invoke-Py -Arguments @("-m","venv",$venv) -LogFile $LogFile -WorkingDirectory $TARGET) - } - $venvPy = Get-VenvPython -VenvDir $venv - [void](Invoke-External -Exe $venvPy -Arguments @("-m","pip","install","-r","requirements.txt") -LogFile $LogFile -WorkingDirectory $TARGET) - # Ensure SDK can find pytest on PATH - $env:PATH = (Join-Path $venv 'Scripts') + ";" + $env:PATH - - $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME - $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY - - # Prepare platform-specific YAMLs in android/ and ios/ - $originalPlatform = $APP_PLATFORM - - $script:APP_PLATFORM = "android" - $platformYamlAndroid = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_MOBILE - $localFlag = if ($UseLocal) { "true" } else { "false" } - $androidYmlPath = Join-Path $TARGET "android\browserstack.yml" -@" -userName: $BROWSERSTACK_USERNAME -accessKey: $BROWSERSTACK_ACCESS_KEY -framework: pytest -browserstackLocal: $localFlag -buildName: browserstack-build-mobile -projectName: NOW-Mobile-Test -parallelsPerPlatform: $ParallelsPerPlatform -app: $APP_URL -platforms: -$platformYamlAndroid -"@ | Set-Content $androidYmlPath - - $script:APP_PLATFORM = "ios" - $platformYamlIos = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_MOBILE - $iosYmlPath = Join-Path $TARGET "ios\browserstack.yml" -@" -userName: $BROWSERSTACK_USERNAME -accessKey: $BROWSERSTACK_ACCESS_KEY -framework: pytest -browserstackLocal: $localFlag -buildName: browserstack-build-mobile -projectName: NOW-Mobile-Test -parallelsPerPlatform: $ParallelsPerPlatform -app: $APP_URL -platforms: -$platformYamlIos -"@ | Set-Content $iosYmlPath - - $script:APP_PLATFORM = $originalPlatform - - Log-Line "✅ Wrote platform YAMLs to android/browserstack.yml and ios/browserstack.yml" $GLOBAL_LOG - - # Replace sample tests in both android and ios with universal, locator-free test - $testContent = @" -import pytest - - -@pytest.mark.usefixtures('setWebdriver') -class TestUniversalAppCheck: - - def test_app_health_check(self): - - # 1. Get initial app and device state (no locators) - initial_package = self.driver.current_package - initial_activity = self.driver.current_activity - initial_orientation = self.driver.orientation - - # 2. Log the captured data to BrowserStack using 'annotate' - log_data = f"Initial State: Package='{initial_package}', Activity='{initial_activity}', Orientation='{initial_orientation}'" - self.driver.execute_script( - 'browserstack_executor: {"action": "annotate", "arguments": {"data": "' + log_data + '", "level": "info"}}' - ) - - # 3. Perform a locator-free action: change device orientation - self.driver.orientation = 'LANDSCAPE' - - # 4. Perform locator-free assertions - assert self.driver.orientation == 'LANDSCAPE' - - # 5. Log the successful state change - self.driver.execute_script( - 'browserstack_executor: {"action": "annotate", "arguments": {"data": "Successfully changed orientation to LANDSCAPE", "level": "info"}}' - ) - - # 6. Set the final session status to 'passed' - self.driver.execute_script( - 'browserstack_executor: {"action": "setSessionStatus", "arguments": {"status": "passed", "reason": "App state verified and orientation changed!"}}' - ) -"@ - $androidTestPath = Join-Path $TARGET "android\bstack_sample.py" - $iosTestPath = Join-Path $TARGET "ios\bstack_sample.py" - Set-ContentNoBom -Path $androidTestPath -Value $testContent - Set-ContentNoBom -Path $iosTestPath -Value $testContent - - # Decide which directory to run based on APP_PLATFORM (default to android) - $runDirName = "android" - if ($APP_PLATFORM -eq "ios") { - $runDirName = "ios" - } - $runDir = Join-Path $TARGET $runDirName - - # Check if domain is private - if (Test-DomainPrivate) { - $UseLocal = $true - } - - # Log local flag status - if ($UseLocal) { - Log-Line "⚠️ BrowserStack Local is ENABLED for this run." $GLOBAL_LOG - } else { - Log-Line "⚠️ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG - } - - Log-Line "🚀 Running 'cd $runDirName && browserstack-sdk pytest -s bstack_sample.py'" $GLOBAL_LOG - $sdk = Join-Path $venv "Scripts\browserstack-sdk.exe" - Push-Location $runDir - try { - [void](Invoke-External -Exe $sdk -Arguments @('pytest','-s','bstack_sample.py') -LogFile $LogFile -WorkingDirectory (Get-Location).Path) - } finally { - Pop-Location - } - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Setup: Mobile (Java) ===== -function Setup-Mobile-Java { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - $REPO = "now-testng-appium-app-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile $MOBILE_LOG - Log-Line "✅ Cloned repository: $REPO into $TARGET" $GLOBAL_LOG - - Push-Location $TARGET - try { - # Navigate to platform-specific directory - if ($APP_PLATFORM -eq "all" -or $APP_PLATFORM -eq "android") { - Set-Location "android\testng-examples" - } else { - Set-Location "ios\testng-examples" - } - - $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME - $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY - - # YAML config path - $env:BROWSERSTACK_CONFIG_FILE = ".\browserstack.yml" - $platforms = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_MOBILE - $localFlag = if ($UseLocal) { "true" } else { "false" } - - # Append to existing YAML (repo has base config) - $yamlAppend = @" -app: $APP_URL -platforms: -$platforms -"@ - Add-Content -Path $env:BROWSERSTACK_CONFIG_FILE -Value $yamlAppend - - # Check if domain is private - if (Test-DomainPrivate) { - $UseLocal = $true - } - - # Log local flag status - if ($UseLocal) { - Log-Line "✅ BrowserStack Local is ENABLED for this run." $GLOBAL_LOG - } else { - Log-Line "✅ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG - } - - $mvn = Get-MavenCommand -RepoDir (Get-Location).Path - Log-Line "⚙️ Running '$mvn clean'" $GLOBAL_LOG - $cleanExit = Invoke-External -Exe $mvn -Arguments @("clean") -LogFile $LogFile -WorkingDirectory (Get-Location).Path - if ($cleanExit -ne 0) { - Log-Line "❌ 'mvn clean' FAILED. See $LogFile for details." $GLOBAL_LOG - throw "Maven clean failed" - } - - Log-Line "🚀 Running '$mvn test -P sample-test'. This could take a few minutes. Follow the Automation build here: https://automation.browserstack.com/" $GLOBAL_LOG - [void](Invoke-External -Exe $mvn -Arguments @("test","-P","sample-test") -LogFile $LogFile -WorkingDirectory (Get-Location).Path) - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Setup: Mobile (NodeJS) ===== -function Setup-Mobile-NodeJS { - param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile) - - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - - $REPO = "now-webdriverio-appium-app-browserstack" - $TARGET = Join-Path $GLOBAL_DIR $REPO - - New-Item -ItemType Directory -Path $GLOBAL_DIR -Force | Out-Null - if (Test-Path $TARGET) { - Remove-Item -Path $TARGET -Recurse -Force - } - - Invoke-GitClone -Url "https://github.com/BrowserStackCE/$REPO.git" -Target $TARGET -LogFile $MOBILE_LOG - - $testDir = Join-Path $TARGET "test" - Push-Location $testDir - try { - Log-Line "⚙️ Running 'npm install'" $GLOBAL_LOG - [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","install") -LogFile $LogFile -WorkingDirectory $testDir) - - # Generate mobile capabilities JSON file - Log-Line "🧩 Generating mobile capabilities JSON" $GLOBAL_LOG - $usageFile = Join-Path $GLOBAL_DIR "usage_file.json" - [void](Generate-Mobile-Caps-Json -MaxTotalParallels $ParallelsPerPlatform -OutputFile $usageFile) - Log-Line "✅ Created usage_file.json at: $usageFile" $GLOBAL_LOG - - $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME - $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY - $env:BSTACK_PARALLELS = $ParallelsPerPlatform - - Log-Line "🚀 Running 'npm run test'" $GLOBAL_LOG - [void](Invoke-External -Exe "cmd.exe" -Arguments @("/c","npm","run","test") -LogFile $LogFile -WorkingDirectory $testDir) - - } finally { - Pop-Location - Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER) - } -} - -# ===== Wrappers with retry ===== -function Setup-Web { - Log-Line "Starting Web setup for $TECH_STACK" $WEB_LOG - Log-Line "🌐 ========================================" $GLOBAL_LOG - Log-Line "🌐 Starting WEB Testing ($TECH_STACK)" $GLOBAL_LOG - Log-Line "🌐 ========================================" $GLOBAL_LOG - - $localFlag = $false - $attempt = 1 - $success = $true - - $totalParallels = [int]([Math]::Floor($TEAM_PARALLELS_MAX_ALLOWED_WEB * $PARALLEL_PERCENTAGE)) - if ($totalParallels -lt 1) { $totalParallels = 1 } - $parallelsPerPlatform = $totalParallels - - while ($attempt -le 1) { - Log-Line "[Web Setup]" $WEB_LOG - switch ($TECH_STACK) { - "Java" { - Setup-Web-Java -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $WEB_LOG - # Add a small delay to ensure all output is flushed to disk - Start-Sleep -Milliseconds 500 - if (Test-Path $WEB_LOG) { - $content = Get-Content $WEB_LOG -Raw - if ($content -match "BUILD FAILURE") { - $success = $false - } - } - } - "Python" { - Setup-Web-Python -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $WEB_LOG - # Add a small delay to ensure all output is flushed to disk - Start-Sleep -Milliseconds 500 - if (Test-Path $WEB_LOG) { - $content = Get-Content $WEB_LOG -Raw - if ($content -match "BUILD FAILURE") { - $success = $false - } - } - } - "NodeJS" { - Setup-Web-NodeJS -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $WEB_LOG - # Add a small delay to ensure all output is flushed to disk - Start-Sleep -Milliseconds 500 - if (Test-Path $WEB_LOG) { - $content = Get-Content $WEB_LOG -Raw - if ($content -match "([1-9][0-9]*) passed, 0 failed") { - $success = $false - } - } - } - default { Log-Line "Unknown TECH_STACK: $TECH_STACK" $WEB_LOG; return } - } - - if ($success) { - Log-Line "✅ Web setup succeeded." $WEB_LOG - Log-Line "✅ WEB Testing completed successfully" $GLOBAL_LOG - Log-Line "📊 View detailed web test logs: $WEB_LOG" $GLOBAL_LOG - break - } else { - Log-Line "❌ Web setup ended without success; check $WEB_LOG for details" $WEB_LOG - Log-Line "❌ WEB Testing completed with errors" $GLOBAL_LOG - Log-Line "📊 View detailed web test logs: $WEB_LOG" $GLOBAL_LOG - break - } - } -} - - -function Setup-Mobile { - Log-Line "Starting Mobile setup for $TECH_STACK" $MOBILE_LOG - Log-Line "📱 ========================================" $GLOBAL_LOG - Log-Line "📱 Starting MOBILE APP Testing ($TECH_STACK)" $GLOBAL_LOG - Log-Line "📱 ========================================" $GLOBAL_LOG - - $localFlag = $true - $attempt = 1 - $success = $false - - $totalParallels = [int]([Math]::Floor($TEAM_PARALLELS_MAX_ALLOWED_MOBILE * $PARALLEL_PERCENTAGE)) - if ($totalParallels -lt 1) { $totalParallels = 1 } - $parallelsPerPlatform = $totalParallels - - while ($attempt -le 1) { - Log-Line "[Mobile Setup Attempt $attempt] browserstackLocal: $localFlag" $MOBILE_LOG - switch ($TECH_STACK) { - "Java" { Setup-Mobile-Java -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $MOBILE_LOG } - "Python" { Setup-Mobile-Python -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $MOBILE_LOG } - "NodeJS" { Setup-Mobile-NodeJS -UseLocal:$localFlag -ParallelsPerPlatform $parallelsPerPlatform -LogFile $MOBILE_LOG } - default { Log-Line "Unknown TECH_STACK: $TECH_STACK" $MOBILE_LOG; return } - } - - # Add a small delay to ensure all output is flushed to disk (especially important for Java) - Start-Sleep -Milliseconds 500 - - if (!(Test-Path $MOBILE_LOG)) { - $content = "" - } else { - $content = Get-Content $MOBILE_LOG -Raw - } - - $LOCAL_FAILURE = $false - $SETUP_FAILURE = $false - - foreach ($p in $MOBILE_LOCAL_ERRORS) { if ($p -and ($content -match $p)) { $LOCAL_FAILURE = $true; break } } - foreach ($p in $MOBILE_SETUP_ERRORS) { if ($p -and ($content -match $p)) { $SETUP_FAILURE = $true; break } } + Resolve-Test-Type -RunMode $RunMode -CliValue $TT + Resolve-Tech-Stack -RunMode $RunMode -CliValue $TSTACK + } + + # Setup log file path AFTER selections + $logFileName = "{0}_{1}_run_result.log" -f $TEST_TYPE.ToLowerInvariant(), $TECH_STACK.ToLowerInvariant() + $logFile = Join-Path $LOG_DIR $logFileName + if (!(Test-Path $LOG_DIR)) { + New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null + } + '' | Out-File -FilePath $logFile -Encoding UTF8 + Set-RunLogFile $logFile + $script:GLOBAL_LOG = $logFile + $script:WEB_LOG = $logFile + $script:MOBILE_LOG = $logFile + Log-Line "ℹ️ Log file path: $logFile" $GLOBAL_LOG + + # Setup Summary Header + Log-Section "🧭 Setup Summary – BrowserStack NOW" $GLOBAL_LOG + Log-Line "ℹ️ Timestamp: $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))" $GLOBAL_LOG + Log-Line "ℹ️ Run Mode: $RunMode" $GLOBAL_LOG + Log-Line "ℹ️ Selected Testing Type: $TEST_TYPE" $GLOBAL_LOG + Log-Line "ℹ️ Selected Tech Stack: $TECH_STACK" $GLOBAL_LOG + + # Setup workspace and get credentials BEFORE app upload + Setup-Workspace + Ask-BrowserStack-Credentials -RunMode $RunMode -UsernameFromEnv $env:BROWSERSTACK_USERNAME -AccessKeyFromEnv $env:BROWSERSTACK_ACCESS_KEY + + # NOW handle URL/App upload (requires credentials) + Perform-NextSteps-BasedOnTestType -TestType $TEST_TYPE -RunMode $RunMode -TestUrl $TestUrl -AppPath $AppPath -AppPlatform $AppPlatform + + # Platform & Tech Stack section + Log-Section "⚙️ Platform & Tech Stack" $GLOBAL_LOG + Log-Line "ℹ️ Platform: $TEST_TYPE" $GLOBAL_LOG + Log-Line "ℹ️ Tech Stack: $TECH_STACK" $GLOBAL_LOG + + # System Prerequisites Check + Log-Section "🧩 System Prerequisites Check" $GLOBAL_LOG + Validate-Tech-Stack - # Check for BrowserStack link (success indicator) - if ($content -match 'https://[a-zA-Z0-9./?=_-]*browserstack\.com') { - $success = $true - } + # Account & Plan Details + Log-Section "☁️ Account & Plan Details" $GLOBAL_LOG + Fetch-Plan-Details -TestType $TEST_TYPE - if ($success) { - Log-Line "✅ Mobile setup succeeded" $MOBILE_LOG - Log-Line "✅ MOBILE APP Testing completed successfully" $GLOBAL_LOG - Log-Line "📊 View detailed mobile test logs: $MOBILE_LOG" $GLOBAL_LOG - break - } elseif ($LOCAL_FAILURE -and $attempt -eq 1) { - $localFlag = $false - $attempt++ - Log-Line "⚠️ Mobile test failed due to Local tunnel error. Retrying without browserstackLocal..." $MOBILE_LOG - Log-Line "⚠️ Mobile test failed due to Local tunnel error. Retrying without browserstackLocal..." $GLOBAL_LOG - } elseif ($SETUP_FAILURE) { - Log-Line "❌ Mobile test failed due to setup error. Check logs at: $MOBILE_LOG" $MOBILE_LOG - Log-Line "❌ MOBILE APP Testing failed due to setup error" $GLOBAL_LOG - Log-Line "📊 View detailed mobile test logs: $MOBILE_LOG" $GLOBAL_LOG - break - } else { - Log-Line "❌ Mobile setup ended without success; check $MOBILE_LOG for details" $MOBILE_LOG - Log-Line "❌ MOBILE APP Testing completed with errors" $GLOBAL_LOG - Log-Line "📊 View detailed mobile test logs: $MOBILE_LOG" $GLOBAL_LOG - break - } - } -} + Log-Line "Plan summary: WEB_PLAN_FETCHED=$WEB_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_WEB), MOBILE_PLAN_FETCHED=$MOBILE_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_MOBILE)" $GLOBAL_LOG + Log-Line "Checking proxy in environment" $GLOBAL_LOG + Set-ProxyInEnv -Username $BROWSERSTACK_USERNAME -AccessKey $BROWSERSTACK_ACCESS_KEY + # Getting Ready section + Log-Section "🧹 Getting Ready" $GLOBAL_LOG + Log-Line "ℹ️ Detected Operating system: Windows" $GLOBAL_LOG + Log-Line "ℹ️ Clearing old logs from NOW Home Directory inside .browserstack" $GLOBAL_LOG + Clear-OldLogs -# ===== Orchestration ===== -function Run-Setup { - Log-Line "Orchestration: TEST_TYPE=$TEST_TYPE, WEB_PLAN_FETCHED=$WEB_PLAN_FETCHED, MOBILE_PLAN_FETCHED=$MOBILE_PLAN_FETCHED" $GLOBAL_LOG + Log-Line "ℹ️ Starting $TEST_TYPE setup for $TECH_STACK" $GLOBAL_LOG - $webRan = $false - $mobileRan = $false - - switch ($TEST_TYPE) { - "Web" { - if ($WEB_PLAN_FETCHED) { - Setup-Web - $webRan = $true - } else { - Log-Line "⚠️ Skipping Web setup — Web plan not fetched" $GLOBAL_LOG - } - } - "App" { - if ($MOBILE_PLAN_FETCHED) { - Setup-Mobile - $mobileRan = $true - } else { - Log-Line "⚠️ Skipping Mobile setup — Mobile plan not fetched" $GLOBAL_LOG - } - } - "Both" { - $ranAny = $false - if ($WEB_PLAN_FETCHED) { - Setup-Web - $webRan = $true - $ranAny = $true - } else { - Log-Line "⚠️ Skipping Web setup — Web plan not fetched" $GLOBAL_LOG - } - if ($MOBILE_PLAN_FETCHED) { - Setup-Mobile - $mobileRan = $true - $ranAny = $true - } else { - Log-Line "⚠️ Skipping Mobile setup — Mobile plan not fetched" $GLOBAL_LOG - } - if (-not $ranAny) { - Log-Line "❌ Both Web and Mobile setup were skipped. Exiting." $GLOBAL_LOG - throw "No setups executed" - } - } - default { - Log-Line "❌ Invalid TEST_TYPE: $TEST_TYPE" $GLOBAL_LOG - throw "Invalid TEST_TYPE" - } - } - - # Final Summary - Log-Line " " $GLOBAL_LOG - Log-Line "========================================" $GLOBAL_LOG - Log-Line "📋 EXECUTION SUMMARY" $GLOBAL_LOG - Log-Line "========================================" $GLOBAL_LOG - if ($webRan) { - Log-Line "✅ Web Testing: COMPLETED" $GLOBAL_LOG - Log-Line " 📄 Logs: $WEB_LOG" $GLOBAL_LOG - } - if ($mobileRan) { - Log-Line "✅ Mobile App Testing: COMPLETED" $GLOBAL_LOG - Log-Line " 📄 Logs: $MOBILE_LOG" $GLOBAL_LOG - } - Log-Line "========================================" $GLOBAL_LOG - Log-Line "🎉 All requested tests have been executed!" $GLOBAL_LOG - Log-Line "🔗 View results: https://automation.browserstack.com/" $GLOBAL_LOG - Log-Line "========================================" $GLOBAL_LOG -} + # Run the setup + Run-Setup -TestType $TEST_TYPE -TechStack $TECH_STACK -RunMode $RunMode -# ===== Main ===== -try { - Ensure-Workspace - Ask-BrowserStack-Credentials - Ask-Test-Type - Ask-Tech-Stack - Validate-Tech-Stack - Fetch-Plan-Details - - Log-Line "Plan summary: WEB_PLAN_FETCHED=$WEB_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_WEB), MOBILE_PLAN_FETCHED=$MOBILE_PLAN_FETCHED (team max=$TEAM_PARALLELS_MAX_ALLOWED_MOBILE)" $GLOBAL_LOG - - # Check for proxy configuration - Log-Line "ℹ️ Checking proxy in environment" $GLOBAL_LOG - $proxyCheckScript = Join-Path $PSScriptRoot "proxy-check.ps1" - if (Test-Path $proxyCheckScript) { - try { - & $proxyCheckScript -BrowserStackUsername $BROWSERSTACK_USERNAME -BrowserStackAccessKey $BROWSERSTACK_ACCESS_KEY - if ($env:PROXY_HOST -and $env:PROXY_PORT) { - Log-Line "✅ Proxy configured: $env:PROXY_HOST:$env:PROXY_PORT" $GLOBAL_LOG - } else { - Log-Line "ℹ️ No proxy configured or proxy check failed" $GLOBAL_LOG - } - } catch { - Log-Line "⚠️ Proxy check script failed: $($_.Exception.Message)" $GLOBAL_LOG - } - } else { - Log-Line "⚠️ Proxy check script not found at: $proxyCheckScript" $GLOBAL_LOG - } - - Run-Setup } catch { Log-Line " " $GLOBAL_LOG Log-Line "========================================" $GLOBAL_LOG @@ -1751,9 +110,8 @@ try { Log-Line "========================================" $GLOBAL_LOG Log-Line "Error: $($_.Exception.Message)" $GLOBAL_LOG Log-Line "Check logs for details:" $GLOBAL_LOG - Log-Line " Global: $GLOBAL_LOG" $GLOBAL_LOG - Log-Line " Web: $WEB_LOG" $GLOBAL_LOG - Log-Line " Mobile: $MOBILE_LOG" $GLOBAL_LOG + Log-Line (" Run Log: {0}" -f (Get-RunLogFile)) $GLOBAL_LOG Log-Line "========================================" $GLOBAL_LOG throw -} \ No newline at end of file +} + diff --git a/win/user-interaction.ps1 b/win/user-interaction.ps1 new file mode 100644 index 0000000..307bb8e --- /dev/null +++ b/win/user-interaction.ps1 @@ -0,0 +1,382 @@ +# User interaction helpers (GUI + CLI) for Windows BrowserStack NOW. + +function Show-InputBox { + param( + [string]$Title = "Input", + [string]$Prompt = "Enter value:", + [string]$DefaultText = "" + ) + $form = New-Object System.Windows.Forms.Form + $form.Text = $Title + $form.Size = New-Object System.Drawing.Size(500,220) + $form.StartPosition = "CenterScreen" + + $label = New-Object System.Windows.Forms.Label + $label.Text = $Prompt + $label.MaximumSize = New-Object System.Drawing.Size(460,0) + $label.AutoSize = $true + $label.Location = New-Object System.Drawing.Point(10,20) + $form.Controls.Add($label) + + $textBox = New-Object System.Windows.Forms.TextBox + $textBox.Size = New-Object System.Drawing.Size(460,20) + $textBox.Location = New-Object System.Drawing.Point(10,($label.Bottom + 10)) + $textBox.Text = $DefaultText + $form.Controls.Add($textBox) + + $okButton = New-Object System.Windows.Forms.Button + $okButton.Text = "OK" + $okButton.Location = New-Object System.Drawing.Point(380,($textBox.Bottom + 20)) + $okButton.Add_Click({ $form.Tag = $textBox.Text; $form.Close() }) + $form.Controls.Add($okButton) + + $form.AcceptButton = $okButton + [void]$form.ShowDialog() + return [string]$form.Tag +} + +function Show-PasswordBox { + param( + [string]$Title = "Secret", + [string]$Prompt = "Enter secret:" + ) + $form = New-Object System.Windows.Forms.Form + $form.Text = $Title + $form.Size = New-Object System.Drawing.Size(500,220) + $form.StartPosition = "CenterScreen" + + $label = New-Object System.Windows.Forms.Label + $label.Text = $Prompt + $label.MaximumSize = New-Object System.Drawing.Size(460,0) + $label.AutoSize = $true + $label.Location = New-Object System.Drawing.Point(10,20) + $form.Controls.Add($label) + + $textBox = New-Object System.Windows.Forms.TextBox + $textBox.Size = New-Object System.Drawing.Size(460,20) + $textBox.Location = New-Object System.Drawing.Point(10,($label.Bottom + 10)) + $textBox.UseSystemPasswordChar = $true + $form.Controls.Add($textBox) + + $okButton = New-Object System.Windows.Forms.Button + $okButton.Text = "OK" + $okButton.Location = New-Object System.Drawing.Point(380,($textBox.Bottom + 20)) + $okButton.Add_Click({ $form.Tag = $textBox.Text; $form.Close() }) + $form.Controls.Add($okButton) + + $form.AcceptButton = $okButton + [void]$form.ShowDialog() + return [string]$form.Tag +} + +function Show-ClickChoice { + param( + [string]$Title = "Choose", + [string]$Prompt = "Select one:", + [string[]]$Choices, + [string]$DefaultChoice + ) + if (-not $Choices -or $Choices.Count -eq 0) { return "" } + + $form = New-Object System.Windows.Forms.Form + $form.Text = $Title + $form.StartPosition = "CenterScreen" + $form.MinimizeBox = $false + $form.MaximizeBox = $false + $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog + $form.BackColor = [System.Drawing.Color]::FromArgb(245,245,245) + + $label = New-Object System.Windows.Forms.Label + $label.Text = $Prompt + $label.AutoSize = $true + $label.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Regular) + $label.Location = New-Object System.Drawing.Point(12, 12) + $form.Controls.Add($label) + + $panel = New-Object System.Windows.Forms.FlowLayoutPanel + $panel.Location = New-Object System.Drawing.Point(12, 40) + $panel.Size = New-Object System.Drawing.Size(460, 140) + $panel.WrapContents = $true + $panel.AutoScroll = $true + $panel.FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight + $form.Controls.Add($panel) + + $selected = $null + foreach ($c in $Choices) { + $btn = New-Object System.Windows.Forms.Button + $btn.Text = $c + $btn.Width = 140 + $btn.Height = 40 + $btn.Margin = '8,8,8,8' + $btn.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) + $btn.FlatStyle = 'System' + if ($c -eq $DefaultChoice) { + $btn.BackColor = [System.Drawing.Color]::FromArgb(232,240,254) + } + $btn.Add_Click({ + $script:selected = $this.Text + $form.Tag = $script:selected + $form.Close() + }) + $panel.Controls.Add($btn) + } + + $cancel = New-Object System.Windows.Forms.Button + $cancel.Text = "Cancel" + $cancel.Width = 90 + $cancel.Height = 32 + $cancel.Location = New-Object System.Drawing.Point(382, 188) + $cancel.Add_Click({ $form.Tag = ""; $form.Close() }) + $form.Controls.Add($cancel) + $form.CancelButton = $cancel + + $form.ClientSize = New-Object System.Drawing.Size(484, 230) + [void]$form.ShowDialog() + return [string]$form.Tag +} + +function Show-OpenFileDialog { + param( + [string]$Title = "Select File", + [string]$Filter = "All files (*.*)|*.*" + ) + $ofd = New-Object System.Windows.Forms.OpenFileDialog + $ofd.Title = $Title + $ofd.Filter = $Filter + $ofd.Multiselect = $false + if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + return $ofd.FileName + } + return "" +} + +function Ask-BrowserStack-Credentials { + param( + [string]$RunMode = "--interactive", + [string]$UsernameFromEnv, + [string]$AccessKeyFromEnv + ) + if ($RunMode -match "--silent" -or $RunMode -match "--debug") { + $script:BROWSERSTACK_USERNAME = if ($UsernameFromEnv) { $UsernameFromEnv } else { $env:BROWSERSTACK_USERNAME } + $script:BROWSERSTACK_ACCESS_KEY = if ($AccessKeyFromEnv) { $AccessKeyFromEnv } else { $env:BROWSERSTACK_ACCESS_KEY } + if ([string]::IsNullOrWhiteSpace($script:BROWSERSTACK_USERNAME) -or [string]::IsNullOrWhiteSpace($script:BROWSERSTACK_ACCESS_KEY)) { + throw "BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY must be provided in silent/debug mode." + } + Log-Line "✅ BrowserStack credentials loaded from environment for user: $script:BROWSERSTACK_USERNAME" $GLOBAL_LOG + return + } + + $script:BROWSERSTACK_USERNAME = Show-InputBox -Title "BrowserStack Setup" -Prompt "Enter your BrowserStack Username:`n`nLocate it on https://www.browserstack.com/accounts/profile/details" -DefaultText "" + if ([string]::IsNullOrWhiteSpace($script:BROWSERSTACK_USERNAME)) { + Log-Line "❌ Username empty" $GLOBAL_LOG + throw "Username is required" + } + $script:BROWSERSTACK_ACCESS_KEY = Show-PasswordBox -Title "BrowserStack Setup" -Prompt "Enter your BrowserStack Access Key:`n`nLocate it on https://www.browserstack.com/accounts/profile/details" + if ([string]::IsNullOrWhiteSpace($script:BROWSERSTACK_ACCESS_KEY)) { + Log-Line "❌ Access Key empty" $GLOBAL_LOG + throw "Access Key is required" + } + Log-Line "✅ BrowserStack credentials captured (access key hidden)" $GLOBAL_LOG +} + +function Resolve-Test-Type { + param( + [string]$RunMode, + [string]$CliValue + ) + if ($RunMode -match "--silent" -or $RunMode -match "--debug") { + if (-not $CliValue) { $CliValue = $env:TEST_TYPE } + if ([string]::IsNullOrWhiteSpace($CliValue)) { throw "TEST_TYPE is required in silent/debug mode." } + $candidate = (Get-Culture).TextInfo.ToTitleCase($CliValue.ToLowerInvariant()) + if ($candidate -notin @("Web","App")) { + throw "TEST_TYPE must be either 'Web' or 'App'." + } + $script:TEST_TYPE = $candidate + return + } + + $choice = Show-ClickChoice -Title "Testing Type" ` + -Prompt "What do you want to run?" ` + -Choices @("Web","App") ` + -DefaultChoice "Web" + if ([string]::IsNullOrWhiteSpace($choice)) { throw "No testing type selected" } + $script:TEST_TYPE = $choice +} + +function Resolve-Tech-Stack { + param( + [string]$RunMode, + [string]$CliValue + ) + if ($RunMode -match "--silent" -or $RunMode -match "--debug") { + if (-not $CliValue) { $CliValue = $env:TECH_STACK } + if ([string]::IsNullOrWhiteSpace($CliValue)) { throw "TECH_STACK is required in silent/debug mode." } + $textInfo = (Get-Culture).TextInfo + $candidate = $textInfo.ToTitleCase($CliValue.ToLowerInvariant()) + if ($candidate -notin @("Java","Python","NodeJS")) { + throw "TECH_STACK must be one of: Java, Python, NodeJS." + } + $script:TECH_STACK = $candidate + return + } + + $choice = Show-ClickChoice -Title "Tech Stack" ` + -Prompt "Select your installed language / framework:" ` + -Choices @("Java","Python","NodeJS") ` + -DefaultChoice "Java" + if ([string]::IsNullOrWhiteSpace($choice)) { throw "No tech stack selected" } + $script:TECH_STACK = $choice +} + +function Ask-User-TestUrl { + param([string]$RunMode,[string]$CliValue) + if ($RunMode -match "--silent" -or $RunMode -match "--debug") { + $script:CX_TEST_URL = if ($CliValue) { $CliValue } elseif ($env:CX_TEST_URL) { $env:CX_TEST_URL } else { $DEFAULT_TEST_URL } + return + } + + $testUrl = Show-InputBox -Title "Test URL Setup" -Prompt "Enter the URL you want to test with BrowserStack:`n(Leave blank for default: $DEFAULT_TEST_URL)" -DefaultText "" + if ([string]::IsNullOrWhiteSpace($testUrl)) { + $testUrl = $DEFAULT_TEST_URL + Log-Line "⚠️ No URL entered. Falling back to default: $testUrl" $GLOBAL_LOG + } else { + Log-Line "🌐 Using custom test URL: $testUrl" $GLOBAL_LOG + } + $script:CX_TEST_URL = $testUrl +} + +function Show-OpenOrSampleAppDialog { + $appChoice = Show-ClickChoice -Title "App Selection" ` + -Prompt "Choose an app to test:" ` + -Choices @("Sample App","Browse") ` + -DefaultChoice "Sample App" + return $appChoice +} + +function Invoke-SampleAppUpload { + $headers = @{ + Authorization = (Get-BasicAuthHeader -User $BROWSERSTACK_USERNAME -Key $BROWSERSTACK_ACCESS_KEY) + } + $body = @{ + url = "https://www.browserstack.com/app-automate/sample-apps/android/WikipediaSample.apk" + } + $resp = Invoke-RestMethod -Method Post -Uri "https://api-cloud.browserstack.com/app-automate/upload" -Headers $headers -ContentType "application/x-www-form-urlencoded" -Body $body + $url = $resp.app_url + if ([string]::IsNullOrWhiteSpace($url)) { + throw "Sample app upload failed" + } + return @{ + Url = $url + Platform = "android" + } +} + +function Invoke-CustomAppUpload { + param( + [Parameter(Mandatory)][string]$FilePath + ) + + $ext = [System.IO.Path]::GetExtension($FilePath).ToLowerInvariant() + switch ($ext) { + ".apk" { $platform = "android" } + ".ipa" { $platform = "ios" } + default { throw "Unsupported app file (only .apk/.ipa)" } + } + + $boundary = [System.Guid]::NewGuid().ToString() + $LF = "`r`n" + $fileBin = [System.IO.File]::ReadAllBytes($FilePath) + $fileName = [System.IO.Path]::GetFileName($FilePath) + + $bodyLines = ( + "--$boundary", + "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", + "Content-Type: application/octet-stream$LF", + [System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileBin), + "--$boundary--$LF" + ) -join $LF + + $headers = @{ + Authorization = (Get-BasicAuthHeader -User $BROWSERSTACK_USERNAME -Key $BROWSERSTACK_ACCESS_KEY) + "Content-Type" = "multipart/form-data; boundary=$boundary" + } + + $resp = Invoke-RestMethod -Method Post -Uri "https://api-cloud.browserstack.com/app-automate/upload" -Headers $headers -Body $bodyLines + $url = $resp.app_url + if ([string]::IsNullOrWhiteSpace($url)) { + throw "Upload failed" + } + return @{ + Url = $url + Platform = $platform + } +} + +function Ask-And-Upload-App { + param( + [string]$RunMode, + [string]$CliPath, + [string]$CliPlatform + ) + + if ($RunMode -match "--silent" -or $RunMode -match "--debug") { + if ($CliPath) { + $result = Invoke-CustomAppUpload -FilePath $CliPath + $script:APP_URL = $result.Url + $script:APP_PLATFORM = if ($CliPlatform) { $CliPlatform } else { $result.Platform } + return + } + $result = Invoke-SampleAppUpload + Log-Line "⚠️ Using auto-uploaded sample app: $($result.Url)" $GLOBAL_LOG + $script:APP_URL = $result.Url + $script:APP_PLATFORM = $result.Platform + return + } + + $choice = Show-OpenOrSampleAppDialog + if ([string]::IsNullOrWhiteSpace($choice) -or $choice -eq "Sample App") { + $result = Invoke-SampleAppUpload + Log-Line "⚠️ Using sample app: $($result.Url)" $GLOBAL_LOG + $script:APP_URL = $result.Url + $script:APP_PLATFORM = $result.Platform + return + } + + $path = Show-OpenFileDialog -Title "📱 Select your .apk or .ipa file" -Filter "App Files (*.apk;*.ipa)|*.apk;*.ipa|All files (*.*)|*.*" + if ([string]::IsNullOrWhiteSpace($path)) { + $result = Invoke-SampleAppUpload + Log-Line "⚠️ No app selected. Using sample app: $($result.Url)" $GLOBAL_LOG + $script:APP_URL = $result.Url + $script:APP_PLATFORM = $result.Platform + return + } + + $result = Invoke-CustomAppUpload -FilePath $path + $script:APP_URL = $result.Url + $script:APP_PLATFORM = $result.Platform + Log-Line "✅ App uploaded successfully: $($result.Url)" $GLOBAL_LOG +} + +# ===== Perform next steps based on test type (like Mac's perform_next_steps_based_on_test_type) ===== +function Perform-NextSteps-BasedOnTestType { + param( + [string]$TestType, + [string]$RunMode, + [string]$TestUrl, + [string]$AppPath, + [string]$AppPlatform + ) + + switch -Regex ($TestType) { + "^Web$|^web$" { + Ask-User-TestUrl -RunMode $RunMode -CliValue $TestUrl + } + "^App$|^app$" { + Ask-And-Upload-App -RunMode $RunMode -CliPath $AppPath -CliPlatform $AppPlatform + } + default { + throw "Unsupported TEST_TYPE: $TestType. Allowed values: Web, App." + } + } +} +