feat(scripts): switch Node automatically, and add a test runner (#140)
start-local.ps1 knew exactly what was wrong when Node was too old and then made you fix it by hand. Assert-NodeVersion read node --version, found a major below 20, and threw a message telling you to run `nvm use 24.13.1` and start again in a new shell. A good error for a problem the script could simply solve — and since nvm's default here is 18.16.1, it was hit on every fresh shell. It now runs `nvm use latest` itself and -Stop puts the machine back to 18.16.1. The revert also runs when a start fails partway: without that, a run dying in migrations leaves the machine switched with nothing started, and the -Stop that would restore it is never reached. nvm rewrites a machine-global symlink rather than changing one shell, so this changes the Node version for every terminal on the machine while a script runs. That is the intent — the point is to work in whatever shell is already open — but it is announced every time rather than done quietly. The switch is verified rather than trusted. nvm-windows exits 0 for switches that did not happen: a version it cannot find, a symlink it cannot rewrite without elevation, and — observed here — a rewrite issued immediately after another one, where the directory symlink is briefly still the old target. That last case turned up while testing this change: `nvm use latest` reported success and left Node on 18.16.1. So the result is read back and retried once, and nvm's own output is captured rather than discarded, because suppressing it hid the only message that explained the failure. run-tests.ps1 runs the suites: -Suite unit|integration|e2e|all. One script with a parameter rather than three, because the version switch, the database bring-up and the TEST_PGPORT handling are shared and three copies would drift. The integration suite gets its own throwaway Postgres started and stopped around it, in a finally so a failing suite still tidies up. The e2e suite checks the backend is answering first and says what to start, rather than leaving twenty-five specs to fail on a refused connection that names nothing. `all` runs cheapest and most isolated first, so a break several suites would show is reported by the one that localises it best. The version switching lives in scripts/NodeVersion.ps1, dot-sourced by both, since two copies of it would drift and the half that drifts is the half nobody runs. Closes #140
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
#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 latest installed version 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 first.
|
||||
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,
|
||||
[string]$DefaultNodeVersion = '18.16.1'
|
||||
)
|
||||
|
||||
$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')
|
||||
|
||||
# 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-NodeLatest @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
|
||||
}
|
||||
Reference in New Issue
Block a user