Files
redefined-designs/scripts/run-tests.ps1
T
bermudalambandClaude Opus 5 6a90e957f2
Linting / lint (pull_request) Successful in 2m33s
SonarQube Analysis / sonarqube (pull_request) Successful in 28m49s
test(e2e): give the suite a throwaway database of its own (#186)
The e2e suite ran against the development database and nothing truncated it. Every run seeded more fixtures and left them, so the unfiltered storefront grew monotonically — 1,662 items by the time #186 was filed — until rendering it outran the assertions' timeouts. It failed locally, passed in CI where the database is fresh, and got steadily worse, which is the combination nobody can act on.

start-local.ps1 -E2eDb runs the stack against a separate container on a separate port with tmpfs storage, so it starts empty every time. Migrations already run on every start, so an empty volume is a working one. The development database is untouched, so anything set up there by hand survives.

Deliberately a third database rather than sharing either existing one. The integration suite truncates between tests, so an e2e run sharing with it would have its fixtures deleted underneath it (#116) — different container, different port, different credentials, so the mistake is impossible rather than discouraged.

Verified by recreating the database from the compose file, confirming it came up with zero items, and running the full suite against it: 157 of 157. An earlier attempt at this reported 23 passed and 53 not run, which was worthless — a stale backend from a previous run still held port 3000 and was talking to a database I had already deleted. The port is checked before the run now, and the same mistake produced a wrong rate-limiter measurement earlier in this work.

Pagination is the other half of #186 and is filed separately: a shop that renders its whole catalogue in one page is worth fixing on its own merits, not as a side effect of a test fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:57:02 -05:00

203 lines
7.5 KiB
PowerShell

#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 }
}
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'
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
}