diff --git a/win/run.ps1 b/win/run.ps1
index 4492bda..6ead69c 100644
--- a/win/run.ps1
+++ b/win/run.ps1
@@ -1,817 +1,1774 @@
-# BrowserStack Onboarding Script - Windows PowerShell version
+#requires -version 5.0
+<#
+ BrowserStack Onboarding (PowerShell 5.0, GUI)
+ - Full parity port of macOS bash
+ - Uses WinForms for GUI prompts
+ - Logs to %USERPROFILE%\.browserstack\NOW\logs
+#>
-param(
- [Alias("zlogs")][Switch]$ShowLogs = $false
+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|Safari",
+ "OS X|Monterey|Chrome",
+ "OS X|Ventura|Chrome",
+ #"OS X|Big Sur|Safari",
+ "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
-#================================================================================
-#region Custom UI Function
-#================================================================================
-
-function Show-CustomSelectionForm {
- # Load required .NET assemblies for building a GUI
- Add-Type -AssemblyName System.Windows.Forms
- Add-Type -AssemblyName System.Drawing
-
- # --- Define the Refined Theme ---
- $theme = @{
- BackColor = [System.Drawing.ColorTranslator]::FromHtml("#2D2D30")
- ForeColor = [System.Drawing.ColorTranslator]::FromHtml("#F1F1F1")
- PrimaryAction = [System.Drawing.ColorTranslator]::FromHtml("#0070f0")
- ButtonColor = [System.Drawing.ColorTranslator]::FromHtml("#555555")
- Font = New-Object System.Drawing.Font("Segoe UI", 11)
- HeaderFont = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold)
- }
-
- # --- Create the Main Form ---
- $form = New-Object System.Windows.Forms.Form
- $form.Text = "BrowserStack Onboarding"
- $form.Size = New-Object System.Drawing.Size(480, 480)
- $form.BackColor = $theme.BackColor
- $form.ForeColor = $theme.ForeColor
- $form.Font = $theme.Font
- $form.StartPosition = "CenterScreen"
- $form.FormBorderStyle = "FixedDialog"
- $form.MaximizeBox = $false
- $form.MinimizeBox = $false
+# ===== 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 Credential Controls ---
- $usernameLabel = New-Object System.Windows.Forms.Label
- $usernameLabel.Text = "BrowserStack Username:"
- $usernameLabel.Location = New-Object System.Drawing.Point(40, 40)
- $usernameLabel.AutoSize = $true
- $form.Controls.Add($usernameLabel)
-
- $usernameTextBox = New-Object System.Windows.Forms.TextBox
- $usernameTextBox.Location = New-Object System.Drawing.Point(260, 37)
- $usernameTextBox.Size = New-Object System.Drawing.Size(170, 20)
- $form.Controls.Add($usernameTextBox)
-
- $accessKeyLabel = New-Object System.Windows.Forms.Label
- $accessKeyLabel.Text = "BrowserStack Access Key:"
- $accessKeyLabel.Location = New-Object System.Drawing.Point(40, 75)
- $accessKeyLabel.AutoSize = $true
- $form.Controls.Add($accessKeyLabel)
-
- $accessKeyTextBox = New-Object System.Windows.Forms.TextBox
- $accessKeyTextBox.Location = New-Object System.Drawing.Point(260, 72)
- $accessKeyTextBox.Size = New-Object System.Drawing.Size(170, 20)
- $accessKeyTextBox.UseSystemPasswordChar = $true
- $form.Controls.Add($accessKeyTextBox)
-
-
- # --- Create Test Type Section ---
- $testTypeHeader = New-Object System.Windows.Forms.Label
- $testTypeHeader.Text = "Testing Type"
- $testTypeHeader.Font = $theme.HeaderFont
- $testTypeHeader.Location = New-Object System.Drawing.Point(36, 130)
- $testTypeHeader.AutoSize = $true
- $form.Controls.Add($testTypeHeader)
-
- # === FIX: Create an invisible Panel to group the radio buttons ===
- $testTypePanel = New-Object System.Windows.Forms.Panel
- $testTypePanel.Location = New-Object System.Drawing.Point(38, 160)
- $testTypePanel.Size = New-Object System.Drawing.Size(200, 100)
- $form.Controls.Add($testTypePanel)
-
- $testOptions = @("Web Testing", "Mobile App Testing", "Both")
- $yPos = 5
- foreach ($option in $testOptions) {
- $rb = New-Object System.Windows.Forms.RadioButton
- $rb.Text = $option
- $rb.Location = New-Object System.Drawing.Point(2, $yPos) # Location is relative to the Panel
- $rb.AutoSize = $true
- $testTypePanel.Controls.Add($rb) # Add to the Panel, not the Form
- $yPos += 30
- }
-
- # --- Create Tech Stack Section ---
- $techStackHeader = New-Object System.Windows.Forms.Label
- $techStackHeader.Text = "Technology Stack"
- $techStackHeader.Font = $theme.HeaderFont
- $techStackHeader.Location = New-Object System.Drawing.Point(36, 265)
- $techStackHeader.AutoSize = $true
- $form.Controls.Add($techStackHeader)
+ # 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
+ }
+ }
- # === FIX: Create another invisible Panel for the second group ===
- $techStackPanel = New-Object System.Windows.Forms.Panel
- $techStackPanel.Location = New-Object System.Drawing.Point(38, 295)
- $techStackPanel.Size = New-Object System.Drawing.Size(200, 100)
- $form.Controls.Add($techStackPanel)
-
- $techOptions = @("Java", "Python", "JavaScript")
- $yPos = 5
- foreach ($option in $techOptions) {
- $rb = New-Object System.Windows.Forms.RadioButton
- $rb.Text = $option
- $rb.Location = New-Object System.Drawing.Point(2, $yPos) # Location is relative to the Panel
- $rb.AutoSize = $true
- $techStackPanel.Controls.Add($rb) # Add to the Panel, not the Form
- $yPos += 30
- }
-
- # --- Create Buttons ---
- $continueButton = New-Object System.Windows.Forms.Button
- $continueButton.Text = "Continue"
- $continueButton.Location = New-Object System.Drawing.Point(240, 395)
- $continueButton.Size = New-Object System.Drawing.Size(95, 35)
- $continueButton.BackColor = $theme.PrimaryAction
- $continueButton.ForeColor = [System.Drawing.Color]::White
- $continueButton.FlatStyle = "Flat"
- $continueButton.FlatAppearance.BorderSize = 0
- $form.Controls.Add($continueButton)
-
- $cancelButton = New-Object System.Windows.Forms.Button
- $cancelButton.Text = "Cancel"
- $cancelButton.Location = New-Object System.Drawing.Point(345, 395)
- $cancelButton.Size = New-Object System.Drawing.Size(85, 35)
- $cancelButton.BackColor = $theme.ButtonColor
- $cancelButton.FlatStyle = "Flat"
- $cancelButton.FlatAppearance.BorderSize = 0
- $form.Controls.Add($cancelButton)
-
- # --- Define Button Actions (Event Handlers) ---
- $continueButton.Add_Click({
- # === FIX: Find checked buttons within their respective Panels ===
- $selectedTestType = $testTypePanel.Controls | Where-Object { $_.Checked }
- $selectedTechStack = $techStackPanel.Controls | Where-Object { $_.Checked }
-
- if ([string]::IsNullOrWhiteSpace($usernameTextBox.Text) `
- -or [string]::IsNullOrWhiteSpace($accessKeyTextBox.Text) `
- -or -not $selectedTestType `
- -or -not $selectedTechStack) {
- [System.Windows.Forms.MessageBox]::Show("Please fill in all fields to continue.", "Validation Error", "OK", "Error")
- } else {
- $form.Tag = [PSCustomObject]@{
- Username = $usernameTextBox.Text
- AccessKey = $accessKeyTextBox.Text
- TestType = $selectedTestType.Text
- TechStack = $selectedTechStack.Text
- }
- $form.DialogResult = [System.Windows.Forms.DialogResult]::OK
- $form.Close()
- }
- })
-
- $cancelButton.Add_Click({
- $form.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
- $form.Close()
- })
+ # 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()
- # --- Show the form and wait for the user ---
- $result = $form.ShowDialog()
+ # 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
+}
- if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
- return $form.Tag
- } else {
- return $null
+# 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)
+
+ $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
}
-#endregion
+# === 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)
-#================================================================================
-# Main Script Body
-#================================================================================
+ $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)
+ }
-# Step 1: Workspace Setup
-$BSS_ROOT = Join-Path $HOME '.browserstack'
-$BSS_SETUP_DIR = Join-Path $BSS_ROOT 'browserstackSampleSetup'
-$LOG_FILE = Join-Path $BSS_SETUP_DIR 'bstackOnboardingLogs.log'
-New-Item -Path $BSS_SETUP_DIR -ItemType Directory -Force | Out-Null
-Set-Location -Path $BSS_SETUP_DIR
-New-Item -Path $LOG_FILE -ItemType File -Force | Out-Null
+ $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
-function Write-Log($message) {
- $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
- Add-Content -Path $LOG_FILE -Value "[${timestamp}] $message"
+ $form.ClientSize = New-Object System.Drawing.Size(484, 230)
+ [void]$form.ShowDialog()
+ return [string]$form.Tag
}
-Write-Log "[Workspace initialized]"
-# Step 2 & 3 Combined: Get all user input from the Custom UI
-Write-Host "Please provide your details in the setup window..."
-$selections = Show-CustomSelectionForm
-if (-not $selections) {
- Write-Host "❌ Operation canceled by user."
- exit 1
+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 ""
}
-# Assign all selections from the UI to the script's variables
-$BS_USERNAME = $selections.Username
-$BS_ACCESS_KEY = $selections.AccessKey
-$TEST_OPTION = $selections.TestType
-$TECH_STACK = $selections.TechStack
+# ===== 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
+}
-Write-Host "✅ User credentials and options captured."
-Write-Log "[User selected: $TEST_OPTION | $TECH_STACK]"
+# === 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 }
+ }
+}
-# Step 4: Validate Required Tools
-Write-Host "ℹ️ Checking prerequisites for $TECH_STACK..."
-Write-Host "📂 Current working directory: $(Get-Location)"
-switch ($TECH_STACK) {
+# === 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" {
- if (-not (Get-Command java -ErrorAction SilentlyContinue)) {
- Write-Host "❌ Java is not installed or not in PATH.`n ❗ Please install Java and add it to PATH."
- exit 1
- }
- $javaVer = & java -version 2>&1
- Write-Host "✅ Java is installed. Version details:`n$javaVer"
+ 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" {
- if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
- Write-Host "❌ Python is not installed or not in PATH.`n ❗ Please install Python 3 and ensure it's in PATH."
- exit 1
+ 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"
}
- $pyVer = & python --version
- Write-Host "✅ python is installed: $pyVer"
+ } catch {
+ Log-Line "❌ Python3 exists but failed to run." $GLOBAL_LOG
+ throw
+ }
}
- "JavaScript" {
- if (-not (Get-Command node -ErrorAction SilentlyContinue) -or -not (Get-Command npm -ErrorAction SilentlyContinue)) {
- Write-Host "❌ Node.js or npm is not installed in PATH.`n ❗ Please install Node.js (which includes npm)."
- exit 1
- }
- $nodeVer = & node -v
- $npmVer = & npm -v
- Write-Host "✅ Node.js is installed: $nodeVer"
- Write-Host "✅ npm is installed: $npmVer"
+
+ "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"
}
-Write-Log "[Prerequisites validated for $TECH_STACK]"
-# Step 5: Fetch Plan Details (BrowserStack API)
-$authInfo = "${BS_USERNAME}:${BS_ACCESS_KEY}"
-$authHeaderValue = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($authInfo))
-$headers = @{ Authorization = "Basic $authHeaderValue" }
-$WEB_PLAN_FETCHED = $false
-$MOBILE_PLAN_FETCHED = $false
-$webUnauthorized = $false
-$mobileUnauthorized = $false
+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
+ } else {
+ $script:CX_TEST_URL = $u
+ Log-Line "🌐 Using custom test URL: $script:CX_TEST_URL" $GLOBAL_LOG
+ }
+}
-# Web Testing plan
-try {
- $respWeb = Invoke-WebRequest -Uri "https://api.browserstack.com/automate/plan.json" -Headers $headers -ErrorAction Stop
- $HTTP_CODE_WEB = $respWeb.StatusCode
- $RESPONSE_WEB_BODY = $respWeb.Content
-} catch {
- $HTTP_CODE_WEB = $_.Exception.Response.StatusCode.value__
- $reader = New-Object IO.StreamReader $_.Exception.Response.GetResponseStream()
- $RESPONSE_WEB_BODY = $reader.ReadToEnd(); $reader.Close()
-}
-if ($HTTP_CODE_WEB -eq 200) {
- $WEB_PLAN_FETCHED = $true
- $planWebJson = $RESPONSE_WEB_BODY | ConvertFrom-Json
- $TEAM_PARALLELS_MAX_ALLOWED_WEB = $planWebJson.parallel_sessions_max_allowed
- Write-Host "✅ Web Testing plan fetched: Max Parallels = $TEAM_PARALLELS_MAX_ALLOWED_WEB"
- Write-Log "[Web Plan] $RESPONSE_WEB_BODY"
-} else {
- Write-Host "❌ Web Testing plan fetch failed (HTTP $HTTP_CODE_WEB)"
- Write-Log "[Web Plan Error] $RESPONSE_WEB_BODY"
- if ($HTTP_CODE_WEB -eq 401) {
- Write-Host "⚠️ Invalid credentials or no Web Testing access."
- $webUnauthorized = $true
- }
-}
-
-# Mobile App Testing plan
-try {
- $respMob = Invoke-WebRequest -Uri "https://api-cloud.browserstack.com/app-automate/plan.json" -Headers $headers -ErrorAction Stop
- $HTTP_CODE_MOBILE = $respMob.StatusCode
- $RESPONSE_MOBILE_BODY = $respMob.Content
-} catch {
- $HTTP_CODE_MOBILE = $_.Exception.Response.StatusCode.value__
- $reader = New-Object IO.StreamReader $_.Exception.Response.GetResponseStream()
- $RESPONSE_MOBILE_BODY = $reader.ReadToEnd(); $reader.Close()
-}
-if ($HTTP_CODE_MOBILE -eq 200) {
- $MOBILE_PLAN_FETCHED = $true
- $planMobJson = $RESPONSE_MOBILE_BODY | ConvertFrom-Json
- $TEAM_PARALLELS_MAX_ALLOWED_MOBILE = $planMobJson.parallel_sessions_max_allowed
- Write-Host "✅ Mobile Testing plan fetched: Max Parallels = $TEAM_PARALLELS_MAX_ALLOWED_MOBILE"
- Write-Log "[Mobile Plan] $RESPONSE_MOBILE_BODY"
-} else {
- Write-Host "❌ Mobile Testing plan fetch failed (HTTP $HTTP_CODE_MOBILE)"
- Write-Log "[Mobile Plan Error] $RESPONSE_MOBILE_BODY"
- if ($HTTP_CODE_MOBILE -eq 401) {
- Write-Host "⚠️ Invalid credentials or no Mobile Testing access."
- $mobileUnauthorized = $true
- }
-}
-# Decide exit if no access based on user selection
-if ($TEST_OPTION -eq "Web Testing" -and $webUnauthorized) { exit 1 }
-if ($TEST_OPTION -eq "Mobile App Testing" -and $mobileUnauthorized) { exit 1 }
-if ($TEST_OPTION -eq "Both" -and $webUnauthorized -and $mobileUnauthorized) {
- Write-Host "❌ Both Web and Mobile testing are unavailable with current subscription. Exiting."
- exit 1
-}
-Write-Log "[Plan details fetched]"
-
-# Step 6: Prepare platform templates and YAML generation
-$PARALLEL_PERCENTAGE = 0.75
-$WEB_PLATFORM_TEMPLATES = @(
- "Windows|10|Chrome",
- "Windows|10|Firefox",
- "Windows|11|Edge",
- "Windows|11|Chrome",
- "OS X|Monterey|Safari",
- "OS X|Monterey|Chrome",
- "OS X|Ventura|Chrome",
- "OS X|Big Sur|Safari",
- "OS X|Catalina|Firefox"
-)
-$MOBILE_DEVICE_TEMPLATES = @(
- # Samsung
- "android|Samsung Galaxy S21|11",
- "android|Samsung Galaxy S25|15",
- "android|Samsung Galaxy S24|14",
- "android|Samsung Galaxy S22|12",
- "android|Samsung Galaxy S23|13",
- "android|Samsung Galaxy S21|12",
- "android|Samsung Galaxy Tab S10 Plus|15",
- "android|Samsung Galaxy S22 Ultra|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 S9|8",
- "android|Samsung Galaxy Tab S8|12",
- "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 S8|7",
- "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",
-
- # Google Pixel
- "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|Google Pixel 3|9",
- "android|Google Pixel 9|16",
- "android|Google Pixel 6 Pro|12",
- "android|Google Pixel 6 Pro|15",
- "android|Google Pixel 9 Pro XL|15",
- "android|Google Pixel 9 Pro|15",
- "android|Google Pixel 8 Pro|14",
- "android|Google Pixel 7 Pro|13",
- "android|Google Pixel 5|11",
- "android|Google Pixel 5|12",
- "android|Google Pixel 4 XL|10",
-
- # Vivo
- "android|Vivo Y21|11",
- "android|Vivo Y50|10",
- "android|Vivo V30|14",
- "android|Vivo V21|11",
-
- # Oppo
- "android|Oppo Reno 6|11",
- "android|Oppo Reno 8T 5G|13",
- "android|Oppo A96|11",
- "android|Oppo Reno 3 Pro|10",
-
- # Realme
- "android|Realme 8|11",
-
- # Motorola
- "android|Motorola Moto G71 5G|11",
- "android|Motorola Moto G9 Play|10",
- "android|Motorola Moto G7 Play|9",
-
- # OnePlus
- "android|OnePlus 12R|14",
- "android|OnePlus 11R|13",
- "android|OnePlus 9|11",
- "android|OnePlus 8|10",
-
- # Xiaomi
- "android|Xiaomi Redmi Note 13 Pro 5G|14",
- "android|Xiaomi Redmi Note 12 4G|13",
- "android|Xiaomi Redmi Note 11|11",
- "android|Xiaomi Redmi Note 9|10",
- "android|Xiaomi Redmi Note 8|9",
-
- # Huawei
- "android|Huawei P30|9"
-)
+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 }
+ }
-function Get-WebPlatformsYaml($maxParallels) {
- $max = [int]([math]::Floor($maxParallels * $PARALLEL_PERCENTAGE))
- if ($max -lt 1) { $max = 1 }
- $yamlLines = @(); $count = 0
- foreach ($template in $WEB_PLATFORM_TEMPLATES) {
- if ($count -ge $max) { break }
- $parts = $template -split '\|'
- $os = $parts[0]; $osVer = $parts[1]; $browser = $parts[2]
- foreach ($ver in @("latest", "latest-1", "latest-2")) {
- $yamlLines += " - os: $os"
- $yamlLines += " osVersion: $osVer"
- $yamlLines += " browserName: $browser"
- $yamlLines += " browserVersion: $ver"
- $count++
- if ($count -ge $max) { break }
+ [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 }
}
- return $yamlLines -join [Environment]::NewLine
+ 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
}
-function Get-MobilePlatformsYaml($maxParallels) {
- $max = [int]([math]::Floor($maxParallels * $PARALLEL_PERCENTAGE))
- if ($max -lt 1) { $max = 1 }
- $yamlLines = @(); $count = 0
- foreach ($template in $MOBILE_DEVICE_TEMPLATES) {
- if ($count -ge $max) { break }
- $parts = $template -split '\|'
- $platform = $parts[0]; $device = $parts[1]; $version = $parts[2]
- $yamlLines += " - platformName: $platform"
- $yamlLines += " deviceName: $device"
- $yamlLines += " platformVersion: '$version.0'"
- $count++
- if ($count -ge $max) { break }
+
+# ===== 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
}
- return $yamlLines -join [Environment]::NewLine
+ }
+ 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"
+ }
}
-# Step 7: Define test run functions and patterns
-$WEB_SETUP_ERRORS = @("Error", "Exception", "Build failed", "Session not created", "Cannot start test")
-$WEB_LOCAL_ERRORS = @("browserstack local failed", "fail local testing", "failed to connect tunnel")
-$MOBILE_LOCAL_ERRORS = @("tunnel connection error")
-$MOBILE_SETUP_ERRORS = @()
+# ===== Setup: Web (Java) =====
+function Setup-Web-Java {
+ param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile)
-function Run-WebTests([bool]$useLocal) {
- switch ($TECH_STACK) {
- "Java" { $REPO="testng-browserstack"; $cloneUrl="https://github.com/browserstack/testng-browserstack.git" }
- "Python" { $REPO="python-selenium-browserstack"; $cloneUrl="https://github.com/browserstack/python-selenium-browserstack.git" }
- "JavaScript" { $REPO="webdriverio-browserstack"; $cloneUrl="https://github.com/browserstack/webdriverio-browserstack.git" }
+ $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 {
+ # Update Base URL
+ $files = Get-ChildItem -Path $TARGET -Recurse -Filter *.* -File | Where-Object { $_.Extension -match '\.(java|xml|properties)$' }
+ foreach ($file in $files) {
+ $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
+ if ($content -and $content -match "https://www\.bstackdemo\.com") {
+ $content = $content -replace "https://www\.bstackdemo\.com", $CX_TEST_URL
+ Set-ContentNoBom -Path $file.FullName -Value $content
+ Log-Line "🌐 Updated base URL in $($file.Name)" $GLOBAL_LOG
+ }
}
- if (-not (Test-Path $REPO)) {
- if ($cloneBranch) {
- & git clone $cloneUrl -b $cloneBranch
- } else {
- & git clone $cloneUrl
- }
+
+ # Check if domain is private
+ if (Test-DomainPrivate) {
+ $UseLocal = $true
}
- Set-Location -Path $REPO
- # (Prerequisites check could be repeated here if needed)
+ # 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
+ }
- $platformYaml = Get-WebPlatformsYaml $TEAM_PARALLELS_MAX_ALLOWED_WEB
- if ($TECH_STACK -ne "JavaScript") {
- # Determine framework name
- if ($TECH_STACK -eq "Java") {
- $framework = "testng"
- } else {
- $framework = "python"
- }
+ # 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" }
- if ($framework -eq "testng") {
- $yamlContent = @"
-userName: $BS_USERNAME
-accessKey: $BS_ACCESS_KEY
-framework: $framework
-browserstackLocal: $useLocal
-buildName: browserstack-build-web
-projectName: BrowserStack Web Sample
+ $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:
-$platformYaml
+$platforms
+parallelsPerPlatform: $ParallelsPerPlatform
"@
- } else {
- $yamlContent = @"
-userName: $BS_USERNAME
-accessKey: $BS_ACCESS_KEY
-framework: $framework
-browserstackLocal: $useLocal
-buildName: browserstack-build-web
-projectName: BrowserStack Web Sample
+
+ 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:
-$platformYaml
-"@
- }
- Set-Content -Path "browserstack.yml" -Value $yamlContent
- }
-
- $runLog = Join-Path $BSS_SETUP_DIR 'web_run_result.log'
- if (Test-Path $runLog) { Remove-Item $runLog }
- if ($TECH_STACK -eq "Java") {
- & mvn test -P sample-test *> $runLog 2>&1
- } elseif ($TECH_STACK -eq "Python") {
- & python -m venv env; . .\env\Scripts\Activate.ps1
- & python -m pip install -r requirements.txt >> $LOG_FILE 2>&1
- & browserstack-sdk python .\tests\test.py *> $runLog 2>&1
- } elseif ($TECH_STACK -eq "JavaScript") {
- & npm install >> $LOG_FILE 2>&1
- $confFilePath = "conf/test.conf.js"
- try {
- $configJson = Get-Content $confFilePath -Raw | ConvertFrom-Json
- } catch {
- $configJson = @{}
- }
- $configJson.maxInstances = $TEAM_PARALLELS_MAX_ALLOWED_WEB
- $capList = @(); $count = 0
- $maxCaps = [int]([math]::Floor($TEAM_PARALLELS_MAX_ALLOWED_WEB * $PARALLEL_PERCENTAGE))
- if ($maxCaps -lt 1) { $maxCaps = 1 }
- foreach ($template in $WEB_PLATFORM_TEMPLATES) {
- if ($count -ge $maxCaps) { break }
- $parts = $template -split '\|'
- $os = $parts[0]; $osVer = $parts[1]; $browser = $parts[2]
- foreach ($ver in @("latest", "latest-1", "latest-2")) {
- if ($count -ge $maxCaps) { break }
- $capList += @{
- browserName = $browser
- browserVersion = $ver
- "bstack:options" = @{
- os = $os
- osVersion = $osVer
- }
- }
- $count++
- if ($count -ge $maxCaps) { break }
- }
- }
- $configJson.capabilities = $capList
- ($configJson | ConvertTo-Json -Depth 6) | Set-Content $confFilePath
- $env:BROWSERSTACK_USERNAME = $BS_USERNAME
- $env:BROWSERSTACK_ACCESS_KEY = $BS_ACCESS_KEY
- $env:BROWSERSTACK_LOCAL = $useLocal.ToString().ToLower()
- if ($useLocal) {
- & npm run local *> $runLog 2>&1
- } else {
- & npm run test *> $runLog 2>&1
- }
+$platforms
+parallelsPerPlatform: $ParallelsPerPlatform
+"@ | Set-Content "browserstack.yml"
+
+ Log-Line "✅ Updated root-level browserstack.yml with platforms and credentials" $GLOBAL_LOG
+
+ # Update base URL in test file
+ $testFile = "tests\bstack-sample-test.py"
+ $testFileFull = Join-Path $TARGET $testFile
+ if (Test-Path $testFileFull) {
+ $c = [System.IO.File]::ReadAllText($testFileFull)
+ $c = $c.Replace("https://bstackdemo.com", $CX_TEST_URL)
+ Set-ContentNoBom -Path $testFileFull -Value $c
+ Log-Line "🌐 Updated base URL in tests/bstack-sample-test.py to: $CX_TEST_URL" $GLOBAL_LOG
}
- if (Test-Path $runLog) { Get-Content $runLog | Add-Content $LOG_FILE }
- Set-Location -Path $BSS_SETUP_DIR
+
+ $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)
+ }
}
-function Run-MobileTests([bool]$useLocal) {
- switch ($TECH_STACK) {
- "Java" { $REPO="testng-appium-app-browserstack"; $cloneUrl="https://github.com/browserstack/testng-appium-app-browserstack.git" }
- "Python" { $REPO="python-appium-app-browserstack"; $cloneUrl="https://github.com/browserstack/python-appium-app-browserstack.git" }
- "JavaScript" { $REPO="webdriverio-appium-app-browserstack"; $cloneUrl="https://github.com/browserstack/webdriverio-appium-app-browserstack.git"; $cloneBranch="sdk" }
+# ===== 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
}
- if (-not (Test-Path $REPO)) {
- if ($cloneBranch) {
- & git clone $cloneUrl -b $cloneBranch
- } else {
- & git clone $cloneUrl
- }
+
+ # 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
}
- Set-Location -Path $REPO
- $platformYaml = Get-MobilePlatformsYaml $TEAM_PARALLELS_MAX_ALLOWED_MOBILE
- if ($TECH_STACK -eq "Java") {
- $yamlPath = "android/testng-examples/browserstack.yml"
- $yamlContent = @"
-userName: $BS_USERNAME
-accessKey: $BS_ACCESS_KEY
-framework: testng
-app: bs://sample.app
+ $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME
+ $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY
+ $env:BROWSERSTACK_LOCAL = if ($UseLocal) { "true" } else { "false" }
+
+ 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:
-$platformYaml
-browserstackLocal: $useLocal
-buildName: browserstack-build-1
-projectName: BrowserStack Sample
-"@
- Set-Content -Path $yamlPath -Value $yamlContent
- Set-Location -Path "android/testng-examples"
- & mvn test -P sample-test *> "$BSS_SETUP_DIR\mobile_run_result.log" 2>&1
- }
- elseif ($TECH_STACK -eq "Python") {
- & python -m venv env; . .\env\Scripts\Activate.ps1
- & python -m pip install -r requirements.txt >> $LOG_FILE 2>&1
- Set-Location -Path "android"
- $yamlContent = @"
-userName: $BS_USERNAME
-accessKey: $BS_ACCESS_KEY
-framework: python
-app: bs://sample.app
+$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:
-$platformYaml
-browserstackLocal: $useLocal
-buildName: browserstack-build-1
-projectName: BrowserStack Sample
+$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!"}}'
+ )
"@
- Set-Content -Path "browserstack.yml" -Value $yamlContent
- & browserstack-sdk python browserstack_sample.py *> "$BSS_SETUP_DIR\mobile_run_result.log" 2>&1
- }
- elseif ($TECH_STACK -eq "JavaScript") {
- Push-Location -Path "android\examples\run-parallel-test"
- & npm install >> $LOG_FILE 2>&1
- $confFile = "parallel.conf.js"
- try {
- $config = Get-Content $confFile -Raw | ConvertFrom-Json
- } catch {
- $config = @{}
- }
- $config.maxInstances = $TEAM_PARALLELS_MAX_ALLOWED_MOBILE
- $capList = @(); $count = 0
- $maxCaps = [int]([math]::Floor($TEAM_PARALLELS_MAX_ALLOWED_MOBILE * $PARALLEL_PERCENTAGE))
- if ($maxCaps -lt 1) { $maxCaps = 1 }
- foreach ($template in $MOBILE_DEVICE_TEMPLATES) {
- if ($count -ge $maxCaps) { break }
- $parts = $template -split '\|'
- $deviceName = $parts[1]; $baseVer = [int]$parts[2]
- foreach ($delta in @(0, -1)) {
- if ($count -ge $maxCaps) { break }
- $version = $baseVer + $delta
- $capList += @{ device = $deviceName; "os_version" = "$version.0" }
- $count++
- if ($count -ge $maxCaps) { break }
- }
- }
- $config.capabilities = $capList
- ($config | ConvertTo-Json -Depth 5) | Set-Content $confFile
- Pop-Location
- Set-Location -Path "android"
- $env:BROWSERSTACK_USERNAME = $BS_USERNAME
- $env:BROWSERSTACK_ACCESS_KEY = $BS_ACCESS_KEY
- $env:BROWSERSTACK_LOCAL = $useLocal.ToString().ToLower()
- if ($useLocal) {
- & npm run local *> "$BSS_SETUP_DIR\mobile_run_result.log" 2>&1
- } else {
- & npm run parallel *> "$BSS_SETUP_DIR\mobile_run_result.log" 2>&1
- }
+ $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
}
- if (Test-Path "$BSS_SETUP_DIR\mobile_run_result.log") {
- Get-Content "$BSS_SETUP_DIR\mobile_run_result.log" | Add-Content $LOG_FILE
+
+ # 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
}
- Set-Location -Path $BSS_SETUP_DIR
+
+ } finally {
+ Pop-Location
+ Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER)
+ }
}
-# Step 8: Run the appropriate setups based on selection
-if ($TEST_OPTION -eq "Web Testing" -and $WEB_PLAN_FETCHED) {
- # Run Web tests (with retry logic)
- $webSuccess = $false; $attempt = 1
- while ($attempt -le 2 -and -not $webSuccess) {
- if ($ShowLogs) {
- Write-Host "`n⏳ Running Web tests (Attempt $attempt, browserstackLocal=$($attempt -eq 1))..."
- } else {
- Write-Host "`n⏳ Please hold on while we prepare the next step in the background..."
- }
- $useLocalFlag = ($attempt -eq 1)
- Write-Log "[Web Setup Attempt $attempt] browserstackLocal: $($useLocalFlag.ToString().ToLower())"
- Run-WebTests -useLocal:$useLocalFlag
- $logContent = Get-Content "$BSS_SETUP_DIR\web_run_result.log" -Raw
- $localFailure = $WEB_LOCAL_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $setupFailure = $WEB_SETUP_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $hasSessionLink = ($logContent -match 'https://.+browserstack\.com.+')
- if ($hasSessionLink) {
- $webSuccess = $true; break
- } elseif ($localFailure -and $attempt -eq 1) {
- Write-Host "❌ Web test failed due to Local tunnel error. Retrying without Local..."
- $attempt++; continue
- } elseif ($setupFailure) {
- Write-Host "❌ Web test failed due to setup error. Check logs for details."
- break
- } else {
- break
- }
+# ===== Setup: Mobile (Java) =====
+function Setup-Mobile-Java {
+ param([bool]$UseLocal, [int]$ParallelsPerPlatform, [string]$LogFile)
+
+ $REPO = "browserstack-examples-appium-testng"
+ $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
+
+ # Update pom.xml sdk version to LATEST (matches mac script)
+ $pom = Join-Path $TARGET "pom.xml"
+ if (Test-Path $pom) {
+ $pomContent = Get-Content $pom -Raw
+ $pomContent = $pomContent -replace '(?s)(browserstack-java-sdk.*?)(.*?)()', '$1LATEST$3'
+ $pomContent | Set-Content $pom
+ Log-Line "🔧 Updated browserstack-java-sdk version to LATEST in pom.xml" $GLOBAL_LOG
+ }
+
+ Push-Location $TARGET
+ try {
+ $env:BROWSERSTACK_USERNAME = $BROWSERSTACK_USERNAME
+ $env:BROWSERSTACK_ACCESS_KEY = $BROWSERSTACK_ACCESS_KEY
+
+ # Update driver init to AndroidDriver (parity with bash)
+ $testBase = Get-ChildItem -Path "src" -Recurse -Filter "TestBase.java" -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($testBase) {
+ (Get-Content $testBase.FullName -Raw) -replace 'new AppiumDriver\(', 'new AndroidDriver(' | Set-Content $testBase.FullName
+ Log-Line "🔧 Updated driver initialization in $($testBase.FullName) to use AndroidDriver" $GLOBAL_LOG
}
- if ($webSuccess) {
- $buildUrl = Select-String -Path $LOG_FILE -Pattern 'https://[A-Za-z0-9./?=_-]*browserstack\.com[A-Za-z0-9./?=_-]*' |
- Select-Object -Last 1 -ExpandProperty Line
- Write-Host "✅ Web test run completed. View your tests here:`n👉 $buildUrl"
+
+ $env:BROWSERSTACK_CONFIG_FILE = "src/test/resources/conf/capabilities/browserstack-parallel.yml"
+ $platforms = Generate-Mobile-Platforms-Yaml -MaxTotalParallels $TEAM_PARALLELS_MAX_ALLOWED_MOBILE
+ $localFlag = if ($UseLocal) { "true" } else { "false" }
+
+@"
+userName: $BROWSERSTACK_USERNAME
+accessKey: $BROWSERSTACK_ACCESS_KEY
+framework: testng
+browserstackLocal: $localFlag
+buildName: browserstack-build-mobile
+projectName: NOW-Mobile-Test
+parallelsPerPlatform: $ParallelsPerPlatform
+accessibility: true
+percy: true
+app: $APP_URL
+platforms:
+$platforms
+"@ | Set-Content $env:BROWSERSTACK_CONFIG_FILE
+
+ Log-Line "✅ Updated $env:BROWSERSTACK_CONFIG_FILE with platforms and credentials" $GLOBAL_LOG
+
+ # 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 {
- $logPath = (Resolve-Path "$BSS_SETUP_DIR\web_run_result.log").Path
- Write-Host "❌ Final Web setup failed.`n Check logs at: $logPath`n If the issue persists, contact support@browserstack.com"
+ Log-Line "✅ BrowserStack Local is DISABLED for this run." $GLOBAL_LOG
}
+
+ $mvn = Get-MavenCommand -RepoDir $TARGET
+ Log-Line "⚙️ Running '$mvn install -DskipTests'" $GLOBAL_LOG
+ [void](Invoke-External -Exe $mvn -Arguments @("install","-DskipTests") -LogFile $LogFile -WorkingDirectory $TARGET)
+
+ Log-Line "🚀 Running '$mvn clean test -P bstack-parallel -Dtest=OrderTest'" $GLOBAL_LOG
+ [void](Invoke-External -Exe $mvn -Arguments @("clean","test","-P","bstack-parallel","-Dtest=OrderTest") -LogFile $LogFile -WorkingDirectory $TARGET)
+
+ } finally {
+ Pop-Location
+ Set-Location (Join-Path $WORKSPACE_DIR $PROJECT_FOLDER)
+ }
}
-elseif ($TEST_OPTION -eq "Mobile App Testing" -and $MOBILE_PLAN_FETCHED) {
- # Run Mobile tests (with retry logic)
- $mobileSuccess = $false; $attempt = 1
- while ($attempt -le 2 -and -not $mobileSuccess) {
- if ($ShowLogs) {
- Write-Host "`n⏳ Running Mobile tests (Attempt $attempt, browserstackLocal=$($attempt -eq 1))..."
- } else {
- Write-Host "`n⏳ Please hold on while we prepare the next step in the background..."
+
+# ===== 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" -Branch "sdk" -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
+ }
}
- $useLocalFlag = ($attempt -eq 1)
- Write-Log "[Mobile Setup Attempt $attempt] browserstackLocal: $($useLocalFlag.ToString().ToLower())"
- Run-MobileTests -useLocal:$useLocalFlag
- $logContent = Get-Content "$BSS_SETUP_DIR\mobile_run_result.log" -Raw
- $localFailure = $MOBILE_LOCAL_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $setupFailure = $MOBILE_SETUP_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $hasSessionLink = ($logContent -match 'https://.+browserstack\.com.+')
- if ($hasSessionLink) {
- $mobileSuccess = $true; break
- } elseif ($localFailure -and $attempt -eq 1) {
- Write-Host "❌ Mobile test failed due to Local tunnel error. Retrying without Local..."
- $attempt++; continue
- } elseif ($setupFailure) {
- Write-Host "❌ Mobile test failed due to setup error. Check logs for details."
- break
- } else {
- break
+ }
+ "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 ($mobileSuccess) {
- $buildUrl = Select-String -Path $LOG_FILE -Pattern 'https://[A-Za-z0-9./?=_-]*browserstack\.com[A-Za-z0-9./?=_-]*' |
- Select-Object -Last 1 -ExpandProperty Line
- Write-Host "✅ Mobile test run completed. View your tests here:`n👉 $buildUrl"
+
+ 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 {
- $logPath = (Resolve-Path "$BSS_SETUP_DIR\mobile_run_result.log").Path
- Write-Host "❌ Final Mobile setup failed.`n Check logs at: $logPath`n If the issue persists, contact support@browserstack.com"
- }
-}
-elseif ($TEST_OPTION -eq "Both") {
- $ranAny = $false
- if ($WEB_PLAN_FETCHED) {
- Write-Host "=== Executing Web Testing ==="
- # Run Web tests (same logic as above)
- $webSuccess = $false; $attempt = 1
- while ($attempt -le 2 -and -not $webSuccess) {
- if ($ShowLogs) {
- Write-Host "`n⏳ Running Web tests (Attempt $attempt, browserstackLocal=$($attempt -eq 1))..."
- } else {
- Write-Host "`n⏳ Please hold on while we prepare the next step in the background..."
- }
- $useLocalFlag = ($attempt -eq 1)
- Write-Log "[Web Setup Attempt $attempt] browserstackLocal: $($useLocalFlag.ToString().ToLower())"
- Run-WebTests -useLocal:$useLocalFlag
- $logContent = Get-Content "$BSS_SETUP_DIR\web_run_result.log" -Raw
- $localFailure = $WEB_LOCAL_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $setupFailure = $WEB_SETUP_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $hasSessionLink = ($logContent -match 'https://.+browserstack\.com.+')
- if ($hasSessionLink) {
- $webSuccess = $true; break
- } elseif ($localFailure -and $attempt -eq 1) {
- Write-Host "❌ Web test failed due to Local tunnel error. Retrying without Local..."; $attempt++; continue
- } elseif ($setupFailure) {
- Write-Host "❌ Web test failed due to setup error. Check logs."
- break
- } else {
- break
- }
- }
- if ($webSuccess) {
- $buildUrl = Select-String -Path $LOG_FILE -Pattern 'https://[A-Za-z0-9./?=_-]*browserstack\.com[A-Za-z0-9./?=_-]*' |
- Select-Object -Last 1 -ExpandProperty Line
- Write-Host "✅ Web test run completed. See results:`n👉 $buildUrl"
- } else {
- $logPath = (Resolve-Path "$BSS_SETUP_DIR\web_run_result.log").Path
- Write-Host "❌ Web tests failed. Check logs at $logPath"
- }
- $ranAny = $true
+ 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 {
- Write-Host "⚠️ Skipping Web setup (Automate plan not available)."
- }
- if ($MOBILE_PLAN_FETCHED) {
- Write-Host "`n=== Executing Mobile App Testing ==="
- $mobileSuccess = $false; $attempt = 1
- while ($attempt -le 2 -and -not $mobileSuccess) {
- if ($ShowLogs) {
- Write-Host "`n⏳ Running Mobile tests (Attempt $attempt, browserstackLocal=$($attempt -eq 1))..."
- } else {
- Write-Host "`n⏳ Please hold on while we prepare the next step in the background..."
- }
- $useLocalFlag = ($attempt -eq 1)
- Write-Log "[Mobile Setup Attempt $attempt] browserstackLocal: $($useLocalFlag.ToString().ToLower())"
- Run-MobileTests -useLocal:$useLocalFlag
- $logContent = Get-Content "$BSS_SETUP_DIR\mobile_run_result.log" -Raw
- $localFailure = $MOBILE_LOCAL_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $setupFailure = $MOBILE_SETUP_ERRORS | ForEach-Object { if ($logContent -match $_) { $_; break } }
- $hasSessionLink = ($logContent -match 'https://.+browserstack\.com.+')
- if ($hasSessionLink) {
- $mobileSuccess = $true; break
- } elseif ($localFailure -and $attempt -eq 1) {
- Write-Host "❌ Mobile test failed due to Local tunnel error. Retrying without Local..."; $attempt++; continue
- } elseif ($setupFailure) {
- Write-Host "❌ Mobile test failed due to setup error. Check logs."
- break
- } else {
- break
- }
- }
- if ($mobileSuccess) {
- $buildUrl = Select-String -Path $LOG_FILE -Pattern 'https://[A-Za-z0-9./?=_-]*browserstack\.com[A-Za-z0-9./?=_-]*' |
- Select-Object -Last 1 -ExpandProperty Line
- Write-Host "✅ Mobile test run completed. See results:`n👉 $buildUrl"
- } else {
- $logPath = (Resolve-Path "$BSS_SETUP_DIR\mobile_run_result.log").Path
- Write-Host "❌ Mobile tests failed. Check logs at $logPath"
- }
- $ranAny = $true
+ $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 } }
+
+ # Check for BrowserStack link (success indicator)
+ if ($content -match 'https://[a-zA-Z0-9./?=_-]*browserstack\.com') {
+ $success = $true
+ }
+
+ 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 {
- Write-Host "⚠️ Skipping Mobile setup (App Automate plan not available)."
+ 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
+ }
+ }
+}
+
+
+# ===== 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
+
+ $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
+ }
}
- if (-not $ranAny) {
- Write-Host "❌ Both Web and Mobile setups were skipped due to plan availability. Exiting."
- exit 1
+ "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://automate.browserstack.com/ (Web) | https://app-automate.browserstack.com/ (Mobile)" $GLOBAL_LOG
+ Log-Line "========================================" $GLOBAL_LOG
+}
+
+# ===== 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
+ Run-Setup
+} catch {
+ Log-Line " " $GLOBAL_LOG
+ Log-Line "========================================" $GLOBAL_LOG
+ Log-Line "❌ EXECUTION FAILED" $GLOBAL_LOG
+ 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 "========================================" $GLOBAL_LOG
+ throw
}
\ No newline at end of file