#Requires -Version 7 <# .SYNOPSIS Runs the project's test suites. .DESCRIPTION One script rather than three, because the Node version switch, the test database bring-up and the TEST_PGPORT handling are shared by more than one suite and would otherwise be copied around and drift apart. Switches Node to the pinned version in scripts/NodeVersion.ps1 for the run and puts the machine default back afterwards, the same way start-local.ps1 does — and for the same reason, since ts-jest and Playwright are subject to the same Node 20 floor as the migrations. .PARAMETER Suite unit Backend Jest unit tests. Needs nothing running. integration Backend Jest integration tests against a disposable Postgres. Brings the container up itself. e2e Frontend Playwright tests. Needs the app stack up; start it with .\scripts\start-local.ps1 -E2eDb first. -E2eDb matters. Without it the suite runs against the development database, which nothing truncates: every run seeds more fixtures and leaves them, the unfiltered storefront grows monotonically, and eventually rendering it outruns the assertions' timeout. It then fails locally, passes in CI where the database is fresh, and gets worse (#186). With it, the stack runs against a throwaway database that starts empty. all All three, in that order — cheapest and most isolated first, so a failure that a later suite would also show up in is reported by the suite that localises it best. .PARAMETER TestDbPort Host port for the integration suite's Postgres. Change it if something else holds the default, or if Hyper-V has reserved it. .PARAMETER KeepTestDb Leave the integration database running afterwards. Useful when re-running the same suite repeatedly; it starts faster. .PARAMETER Filter Passed through to the test runner to select tests by file or name. .EXAMPLE .\scripts\run-tests.ps1 -Suite unit .\scripts\run-tests.ps1 -Suite integration -TestDbPort 55600 .\scripts\run-tests.ps1 -Suite e2e -Filter email-templates .\scripts\run-tests.ps1 -Suite all #> [CmdletBinding()] param( [ValidateSet('unit', 'integration', 'e2e', 'all')] [string]$Suite = 'all', [int]$TestDbPort = 55432, [switch]$KeepTestDb, [string]$Filter, # Resolved after NodeVersion.ps1 is dot-sourced below, not here. A param # block runs before anything else in the script, so $script:DEFAULT_NODE_VERSION # is still $null at this point and using it as the default would silently # restore nothing — leaving the machine on the pinned version, which is the # exact failure the restore exists to prevent. See #208. [string]$DefaultNodeVersion = '' ) $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $Backend = Join-Path $RepoRoot 'backend' $Frontend = Join-Path $RepoRoot 'frontend' $WebPort = 5173 function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan } function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray } function Write-Good { param([string]$Message) Write-Host " $Message" -ForegroundColor Green } # $ErrorActionPreference = 'Stop' does not stop the script when a native # executable exits non-zero, only when a cmdlet throws — and every runner here # is npm. Without this a failing suite prints red and the script reports success. function Invoke-Checked { param([scriptblock]$Command, [string]$What) & $Command if ($LASTEXITCODE -ne 0) { throw "$What failed (exit code $LASTEXITCODE)." } } . (Join-Path $PSScriptRoot 'NodeVersion.ps1') # The one home for this value is NodeVersion.ps1, beside NODE_VERSION. It cannot # be a param default (see the note there), so it is filled in here instead, and # an explicit -DefaultNodeVersion still wins. if (-not $DefaultNodeVersion) { $DefaultNodeVersion = $script:DEFAULT_NODE_VERSION } # Bound once so the shared switcher reports in this script's output style. $NodeOut = @{ Step = ${function:Write-Step}; Note = ${function:Write-Note} } function Assert-Docker { docker info *>$null if ($LASTEXITCODE -ne 0) { throw 'Docker is not running. Start Docker Desktop and try again.' } } function Invoke-UnitSuite { Write-Step 'Backend unit tests' Push-Location $Backend try { if ($Filter) { Invoke-Checked { npm run test:unit -- $Filter } 'Unit tests' } else { Invoke-Checked { npm run test:unit } 'Unit tests' } } finally { Pop-Location } } function Invoke-IntegrationSuite { Assert-Docker # The suite's globalSetup connects on TEST_PGPORT and fails with a clear # message if nothing answers, so the container has to be up first. Reusing a # running one is safe: every test truncates in beforeEach. Write-Step "Starting the test database on port $TestDbPort" $env:TEST_PGPORT = "$TestDbPort" Push-Location $Backend try { Invoke-Checked { npm run db:test:up } 'Test database' Write-Step 'Backend integration tests' try { if ($Filter) { Invoke-Checked { npm run test:integration -- $Filter } 'Integration tests' } else { Invoke-Checked { npm run test:integration } 'Integration tests' } } finally { if ($KeepTestDb) { Write-Note 'leaving the test database up (-KeepTestDb)' } else { # In a finally so a failing suite still tidies up. The data is on # tmpfs, so there is nothing to preserve for a post-mortem. Write-Step 'Stopping the test database' npm run db:test:down *>$null } } } finally { Pop-Location } } <# .SYNOPSIS Points the end-to-end helper at the database the running stack actually uses. .DESCRIPTION Closes both halves of #273. The helper in frontend/tests/e2e/support/db.ts opens its own connection to read a password-reset token, and it defaults to redefined_local on 55500. With start-local.ps1 -E2eDb the application is on redefined_e2e on 55501 instead, so the app wrote to one database and the suite read another. Separately, Invoke-IntegrationSuite sets TEST_PGPORT to the integration database and PowerShell keeps it for the rest of the session, so a -Suite all run leaked that port into the e2e run that followed — with none of the matching credentials, which left the helper offering redefined_local's password to the integration database. Setting all five here overrides that leak as well, which is why one change closes both faults. Throws rather than falling back to a default. A default is what produced both faults: it is always plausible and silently wrong, and a suite that reads the wrong database fails in ways that look like application bugs. #> function Set-E2eDatabaseEnv { $databaseFile = Join-Path $RepoRoot '.local/database.json' if (-not (Test-Path $databaseFile)) { throw @" No .local/database.json, so there is no way to know which database the stack is using — and guessing is what #273 was about. It is written by start-local.ps1 when it brings the database up. Start the stack first: .\scripts\start-local.ps1 (development database) .\scripts\start-local.ps1 -E2eDb (throwaway end-to-end database) "@ } $db = Get-Content $databaseFile -Raw | ConvertFrom-Json $env:TEST_PGHOST = $db.host $env:TEST_PGPORT = "$($db.port)" $env:TEST_PGUSER = $db.user $env:TEST_PGPASSWORD = $db.password $env:TEST_PGDATABASE = $db.database Write-Good "end-to-end helper pointed at $($db.database) on $($db.port)" } function Invoke-E2eSuite { # Playwright's own webServer starts the dev server, but the backend it talks # to is not its job. Checked here rather than left to twenty-five specs # failing on a connection refused, which does not say what is missing. $backendUp = $false try { $null = Invoke-WebRequest 'http://localhost:3000/api/config' -TimeoutSec 2 -UseBasicParsing $backendUp = $true } catch { $backendUp = $false } if (-not $backendUp) { throw @" No backend answering on http://localhost:3000/api/config. The end-to-end suite drives real registration, cart and checkout flows against a running stack. Start it first: .\scripts\start-local.ps1 "@ } Write-Good 'backend is up' # Before Playwright starts, so every spec inherits it — and after the # backend check, so a stack that is not running is reported as that rather # than as a missing state file. Set-E2eDatabaseEnv Write-Note "dev server on $WebPort is started by Playwright if it is not already running" Write-Step 'Frontend end-to-end tests' Push-Location $Frontend try { if ($Filter) { Invoke-Checked { npx playwright test $Filter --project=chromium } 'End-to-end tests' } else { Invoke-Checked { npx playwright test --project=chromium } 'End-to-end tests' } } finally { Pop-Location } } Use-PinnedNode @NodeOut try { switch ($Suite) { 'unit' { Invoke-UnitSuite } 'integration' { Invoke-IntegrationSuite } 'e2e' { Invoke-E2eSuite } 'all' { # Cheapest and most isolated first, so a break that several suites # would show is reported by the one that localises it best. Invoke-UnitSuite Invoke-IntegrationSuite Invoke-E2eSuite } } Write-Host '' Write-Host "Passed ($Suite)." -ForegroundColor Green } finally { Restore-Node -Version $DefaultNodeVersion @NodeOut }