220 lines
8.1 KiB
PowerShell
220 lines
8.1 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateRange(1024, 65535)]
|
|
[int]$Port = 4193,
|
|
[ValidateRange(10, 900)]
|
|
[int]$TimeoutSeconds = 180,
|
|
[string]$BrowserPath = ""
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$serveScript = Join-Path $PSScriptRoot 'serve.ps1'
|
|
$serverProcess = $null
|
|
$browserProcess = $null
|
|
$socket = $null
|
|
$exitCode = 1
|
|
$profilePath = Join-Path ([System.IO.Path]::GetTempPath()) ("micarrsim-tests-" + [guid]::NewGuid().ToString('N'))
|
|
|
|
function Resolve-BrowserPath {
|
|
param([string]$RequestedPath)
|
|
if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) {
|
|
$resolved = [System.IO.Path]::GetFullPath($RequestedPath)
|
|
if (-not [System.IO.File]::Exists($resolved)) {
|
|
throw "Browser executable not found: $resolved"
|
|
}
|
|
return $resolved
|
|
}
|
|
$candidates = @(
|
|
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
|
'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
|
|
'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
|
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
|
)
|
|
foreach ($candidate in $candidates) {
|
|
if ([System.IO.File]::Exists($candidate)) { return $candidate }
|
|
}
|
|
throw 'Microsoft Edge or Google Chrome is required to run browser tests.'
|
|
}
|
|
|
|
function Get-FreeLoopbackPort {
|
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
|
try {
|
|
$listener.Start()
|
|
return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
|
|
}
|
|
finally {
|
|
$listener.Stop()
|
|
}
|
|
}
|
|
|
|
function Wait-ForHttp {
|
|
param([string]$Url, [int]$Seconds)
|
|
$deadline = [DateTime]::UtcNow.AddSeconds($Seconds)
|
|
do {
|
|
try {
|
|
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 2
|
|
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { return }
|
|
}
|
|
catch {
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
} while ([DateTime]::UtcNow -lt $deadline)
|
|
throw "Timed out waiting for $Url"
|
|
}
|
|
|
|
function Send-CdpMessage {
|
|
param([System.Net.WebSockets.ClientWebSocket]$Client, [hashtable]$Message)
|
|
$json = $Message | ConvertTo-Json -Compress -Depth 10
|
|
[byte[]]$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
|
$segment = [System.ArraySegment[byte]]::new($bytes)
|
|
$Client.SendAsync(
|
|
$segment,
|
|
[System.Net.WebSockets.WebSocketMessageType]::Text,
|
|
$true,
|
|
[System.Threading.CancellationToken]::None
|
|
).GetAwaiter().GetResult() | Out-Null
|
|
}
|
|
|
|
function Receive-CdpMessage {
|
|
param([System.Net.WebSockets.ClientWebSocket]$Client)
|
|
$buffer = New-Object byte[] 65536
|
|
$stream = [System.IO.MemoryStream]::new()
|
|
try {
|
|
do {
|
|
$segment = [System.ArraySegment[byte]]::new($buffer)
|
|
$received = $Client.ReceiveAsync(
|
|
$segment,
|
|
[System.Threading.CancellationToken]::None
|
|
).GetAwaiter().GetResult()
|
|
if ($received.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
|
|
throw 'Browser DevTools connection closed before tests completed.'
|
|
}
|
|
$stream.Write($buffer, 0, $received.Count)
|
|
} while (-not $received.EndOfMessage)
|
|
return [System.Text.Encoding]::UTF8.GetString($stream.ToArray()) | ConvertFrom-Json
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
try {
|
|
$browser = Resolve-BrowserPath $BrowserPath
|
|
$debugPort = Get-FreeLoopbackPort
|
|
$serverUrl = "http://127.0.0.1:$Port/"
|
|
$testUrl = "${serverUrl}tests/"
|
|
|
|
$serverArguments = @(
|
|
'-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass',
|
|
'-File', ('"' + $serveScript + '"'), '-Port', [string]$Port, '-NoBrowser'
|
|
)
|
|
$serverProcess = Start-Process -FilePath 'powershell.exe' -ArgumentList $serverArguments -WindowStyle Hidden -PassThru
|
|
Wait-ForHttp -Url $testUrl -Seconds 15
|
|
|
|
[System.IO.Directory]::CreateDirectory($profilePath) | Out-Null
|
|
$browserArguments = @(
|
|
'--headless=new', '--disable-gpu', '--no-first-run', '--disable-background-networking',
|
|
"--remote-debugging-port=$debugPort", ('--user-data-dir="' + $profilePath + '"'), $testUrl
|
|
)
|
|
$browserProcess = Start-Process -FilePath $browser -ArgumentList $browserArguments -WindowStyle Hidden -PassThru
|
|
Wait-ForHttp -Url "http://127.0.0.1:$debugPort/json/version" -Seconds 15
|
|
|
|
$page = $null
|
|
$pageDeadline = [DateTime]::UtcNow.AddSeconds(15)
|
|
do {
|
|
$pages = Invoke-RestMethod -Uri "http://127.0.0.1:$debugPort/json/list" -TimeoutSec 2
|
|
$page = $pages | Where-Object { $_.type -eq 'page' -and $_.url -like "${testUrl}*" } | Select-Object -First 1
|
|
if (-not $page) { Start-Sleep -Milliseconds 100 }
|
|
} while (-not $page -and [DateTime]::UtcNow -lt $pageDeadline)
|
|
if (-not $page) { throw 'Could not find the browser test page.' }
|
|
|
|
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
|
|
$socket.ConnectAsync(
|
|
[uri]$page.webSocketDebuggerUrl,
|
|
[System.Threading.CancellationToken]::None
|
|
).GetAwaiter().GetResult() | Out-Null
|
|
|
|
$timeoutMilliseconds = $TimeoutSeconds * 1000
|
|
$expression = @"
|
|
new Promise((resolve) => {
|
|
const deadline = Date.now() + $timeoutMilliseconds;
|
|
const poll = () => {
|
|
const status = document.documentElement.dataset.testStatus;
|
|
if (status || Date.now() >= deadline) {
|
|
resolve({
|
|
status: status || 'timeout',
|
|
summary: document.querySelector('#test-summary')?.textContent || 'No test summary',
|
|
failures: [...document.querySelectorAll('.test-result--fail')].map((item) => item.innerText),
|
|
});
|
|
return;
|
|
}
|
|
setTimeout(poll, 100);
|
|
};
|
|
poll();
|
|
})
|
|
"@
|
|
Send-CdpMessage -Client $socket -Message @{
|
|
id = 1
|
|
method = 'Runtime.evaluate'
|
|
params = @{ expression = $expression; awaitPromise = $true; returnByValue = $true }
|
|
}
|
|
|
|
$response = $null
|
|
do {
|
|
$candidate = Receive-CdpMessage -Client $socket
|
|
if ($candidate.id -eq 1) { $response = $candidate }
|
|
} while (-not $response)
|
|
if ($response.error) { throw ($response.error | ConvertTo-Json -Compress -Depth 8) }
|
|
if ($response.result.exceptionDetails) {
|
|
throw ($response.result.exceptionDetails | ConvertTo-Json -Compress -Depth 8)
|
|
}
|
|
$testResult = $response.result.result.value
|
|
Write-Host $testResult.summary
|
|
foreach ($failure in @($testResult.failures)) { Write-Host $failure -ForegroundColor Red }
|
|
if ($testResult.status -eq 'pass') {
|
|
Write-Host 'Browser tests passed.' -ForegroundColor Green
|
|
$exitCode = 0
|
|
}
|
|
elseif ($testResult.status -eq 'timeout') {
|
|
Write-Host "Browser tests did not finish within $TimeoutSeconds seconds." -ForegroundColor Red
|
|
}
|
|
else {
|
|
Write-Host 'Browser tests failed.' -ForegroundColor Red
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error $_
|
|
}
|
|
finally {
|
|
if ($socket) {
|
|
try {
|
|
if ($socket.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
|
$socket.CloseAsync(
|
|
[System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure,
|
|
'test complete',
|
|
[System.Threading.CancellationToken]::None
|
|
).GetAwaiter().GetResult() | Out-Null
|
|
}
|
|
}
|
|
catch { }
|
|
$socket.Dispose()
|
|
}
|
|
foreach ($process in @($browserProcess, $serverProcess)) {
|
|
if ($process -and -not $process.HasExited) {
|
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
Start-Sleep -Milliseconds 200
|
|
$temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd([System.IO.Path]::DirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
|
$resolvedProfile = [System.IO.Path]::GetFullPath($profilePath)
|
|
if (
|
|
$resolvedProfile.StartsWith($temporaryRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
|
|
[System.IO.Path]::GetFileName($resolvedProfile).StartsWith('micarrsim-tests-', [System.StringComparison]::Ordinal)
|
|
) {
|
|
Remove-Item -LiteralPath $resolvedProfile -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
exit $exitCode
|