Merge pull request 'feat(scripts): switch Node automatically, and add a test runner (#140)' (#144) from feature/140-node-scripts into main
Reviewed-on: #144
This commit was merged in pull request #144.
This commit is contained in:
@@ -24,6 +24,27 @@ cd redefined-designs
|
||||
|
||||
Commands below are shown for **PowerShell** (Windows). A bash equivalent is noted wherever the syntax differs.
|
||||
|
||||
### The short way
|
||||
|
||||
`scripts/start-local.ps1` does everything in this section — Postgres, migrations, the backend and the dev server — and `scripts/run-tests.ps1` runs the suites. The step-by-step instructions below are still accurate, and are what to reach for when something needs doing differently.
|
||||
|
||||
```powershell
|
||||
.\scripts\start-local.ps1 # bring the whole stack up
|
||||
.\scripts\start-local.ps1 -Fresh # ...from an empty database
|
||||
.\scripts\start-local.ps1 -Stop # stop everything
|
||||
|
||||
.\scripts␍un-tests.ps1 -Suite unit
|
||||
.\scripts␍un-tests.ps1 -Suite integration
|
||||
.\scripts␍un-tests.ps1 -Suite e2e
|
||||
.\scripts␍un-tests.ps1 -Suite all
|
||||
```
|
||||
|
||||
Both scripts run `nvm use latest` first and verify the result is Node 20 or newer, then put the machine back to 18.16.1 when they finish — including when they fail partway, so an interrupted run does not leave the version switched. **`nvm use` rewrites a machine-global symlink, so this changes the Node version for every terminal on the machine while a script is running, not only the one you ran it in.** Both scripts say so as they do it.
|
||||
|
||||
The Node 20 floor is not arbitrary: `node-pg-migrate` pulls in an `lru-cache` that calls `diagnostics_channel.tracingChannel()`, which does not exist before Node 19.9. On Node 18 migrations die inside minified library code with `(0 , U.tracingChannel) is not a function`, which says nothing about versions.
|
||||
|
||||
`run-tests.ps1` brings up whatever a suite needs: the integration suite gets its own throwaway Postgres, started and stopped around the run (`-KeepTestDb` leaves it up, `-TestDbPort` moves it if the default is taken or Hyper-V has reserved it). The e2e suite needs the app stack, so start it with `start-local.ps1` first — the script checks and says so rather than letting every spec fail on a refused connection. `-Filter` passes through to the runner to select tests by file or name.
|
||||
|
||||
### 1. Start a local Postgres instance
|
||||
|
||||
The backend needs Postgres to talk to during local development. `backend/docker-compose.test.yml` spins up a throwaway, tmpfs-backed instance — no data persists between restarts, which is fine for local dev and tests.
|
||||
@@ -155,7 +176,7 @@ npm run db:test:down # when finished
|
||||
|
||||
### Frontend Playwright e2e tests
|
||||
|
||||
Needs the backend running against a database with the schema loaded (steps 1–4 above), since these tests drive real registration/login/purchase flows through a live API.
|
||||
Needs the backend running against a database with the schema loaded (steps 1–4 above, or `.\scripts\start-local.ps1`), since these tests drive real registration/login/purchase flows through a live API.
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Switching the machine's Node version, shared by start-local.ps1 and
|
||||
run-tests.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
Both scripts need Node 20 or newer for the same reason: node-pg-migrate
|
||||
pulls in an lru-cache that calls diagnostics_channel.tracingChannel(), which
|
||||
does not exist before Node 19.9. On Node 18 the migration dies inside
|
||||
minified library code with "(0 , U.tracingChannel) is not a function", a
|
||||
message that says nothing about versions. ts-jest and Playwright are subject
|
||||
to the same floor.
|
||||
|
||||
Kept in one file because two copies of a version switch would drift, and the
|
||||
half that drifts is the half nobody runs.
|
||||
|
||||
nvm-windows rewrites a machine-global symlink (NVM_SYMLINK, typically
|
||||
C:\nvm4w\nodejs) rather than changing one shell, so switching here changes
|
||||
the Node version for every terminal on the machine. That is intended — the
|
||||
point is to work in whatever shell is already open — but it is announced
|
||||
rather than done quietly, and it is put back afterwards.
|
||||
#>
|
||||
|
||||
function Get-NodeVersionString {
|
||||
return (node --version 2>$null)
|
||||
}
|
||||
|
||||
function Get-NodeMajor {
|
||||
$raw = Get-NodeVersionString
|
||||
if (-not $raw) { return 0 }
|
||||
return [int](($raw -replace '^v', '') -split '\.')[0]
|
||||
}
|
||||
|
||||
<#
|
||||
Switches, then checks what is actually running.
|
||||
|
||||
nvm-windows exits 0 for switches that did not take: a version it cannot
|
||||
find, a symlink it cannot rewrite without elevation, and — seen on this
|
||||
machine — a rewrite immediately after another one, where the directory
|
||||
symlink is briefly still the old target. So the result is verified rather
|
||||
than trusted, and retried once, because reporting a version that is not the
|
||||
one running is worse than not switching at all.
|
||||
|
||||
nvm's own output is captured rather than discarded. Suppressing it hid the
|
||||
only message that explained a failed switch.
|
||||
#>
|
||||
function Use-Node {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Version,
|
||||
[Parameter(Mandatory)][string]$Why,
|
||||
[scriptblock]$Step,
|
||||
[scriptblock]$Note
|
||||
)
|
||||
|
||||
if ($Step) { & $Step "Switching Node to $Version ($Why)" }
|
||||
if ($Note) { & $Note 'nvm changes the version for every shell on this machine, not just this one' }
|
||||
|
||||
$output = $null
|
||||
foreach ($attempt in 1..2) {
|
||||
$output = (nvm use $Version 2>&1 | Out-String).Trim()
|
||||
|
||||
$raw = Get-NodeVersionString
|
||||
if ($raw) {
|
||||
# `latest` is whatever nvm decided, so there is nothing to compare a
|
||||
# version string against — the caller checks the major instead.
|
||||
if ($Version -eq 'latest' -or $raw.TrimStart('v') -eq $Version.TrimStart('v')) {
|
||||
if ($Note) { & $Note "node $raw" }
|
||||
return $raw
|
||||
}
|
||||
}
|
||||
|
||||
if ($attempt -eq 1) {
|
||||
if ($Note) { & $Note 'the switch has not taken yet; retrying' }
|
||||
Start-Sleep -Milliseconds 750
|
||||
}
|
||||
}
|
||||
|
||||
$running = Get-NodeVersionString
|
||||
if (-not $running) {
|
||||
throw "Node is not on PATH after 'nvm use $Version'. Check that nvm-windows is installed.`n`nnvm said:`n$output"
|
||||
}
|
||||
|
||||
throw @"
|
||||
Asked nvm for Node $Version, but $running is still what runs.
|
||||
|
||||
nvm said:
|
||||
$output
|
||||
|
||||
nvm-windows rewrites a symlink at $env:NVM_SYMLINK, and can report success
|
||||
without having rewritten it. Check the version is installed, and that this shell
|
||||
can write that link:
|
||||
|
||||
nvm list
|
||||
"@
|
||||
}
|
||||
|
||||
<#
|
||||
Switches to the newest installed version and insists it is new enough.
|
||||
`nvm use latest` picks the highest version nvm has installed locally, which
|
||||
can still be older than this project needs.
|
||||
#>
|
||||
function Use-NodeLatest {
|
||||
param([scriptblock]$Step, [scriptblock]$Note)
|
||||
|
||||
Use-Node -Version 'latest' -Why 'this project needs Node 20 or newer' -Step $Step -Note $Note | Out-Null
|
||||
|
||||
$major = Get-NodeMajor
|
||||
if ($major -lt 20) {
|
||||
throw @"
|
||||
Node is still v$major after 'nvm use latest'. This project needs Node 20 or newer.
|
||||
|
||||
The newest version nvm has installed is too old. Install a newer one:
|
||||
|
||||
nvm install 24.13.1
|
||||
|
||||
Installed versions: nvm list
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
Best effort by design. A failure to switch back must not mask the error that
|
||||
got us here, nor turn a passing run into a failing one — but it must still
|
||||
say so, because leaving the machine on the wrong version silently is how the
|
||||
next confusing failure starts.
|
||||
#>
|
||||
function Restore-Node {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Version,
|
||||
[scriptblock]$Step,
|
||||
[scriptblock]$Note
|
||||
)
|
||||
|
||||
try {
|
||||
Use-Node -Version $Version -Why 'restoring the machine default' -Step $Step -Note $Note | Out-Null
|
||||
}
|
||||
catch {
|
||||
if ($Note) { & $Note "could not restore Node ${Version}: $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+22
-22
@@ -35,7 +35,11 @@ param(
|
||||
[int]$ApiPort = 3000,
|
||||
[int]$WebPort = 5173,
|
||||
[switch]$Fresh,
|
||||
[switch]$Stop
|
||||
[switch]$Stop,
|
||||
# What -Stop puts the machine back to. nvm's default here is 18.16.1, which
|
||||
# is too old to run this project's tooling but is what everything else on
|
||||
# the machine expects.
|
||||
[string]$DefaultNodeVersion = '18.16.1'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -72,26 +76,11 @@ function Assert-Docker {
|
||||
}
|
||||
}
|
||||
|
||||
# node-pg-migrate pulls in an lru-cache that calls
|
||||
# diagnostics_channel.tracingChannel, which does not exist before Node 20. On
|
||||
# Node 18 the migration dies in minified library code with "(0 , U.tracingChannel)
|
||||
# is not a function", which says nothing about versions. CI runs Node 20.
|
||||
function Assert-NodeVersion {
|
||||
$raw = (node --version)
|
||||
$major = [int](($raw -replace '^v', '') -split '\.')[0]
|
||||
if ($major -lt 20) {
|
||||
throw @"
|
||||
Node $raw is too old. This needs Node 20 or newer.
|
||||
. (Join-Path $PSScriptRoot 'NodeVersion.ps1')
|
||||
|
||||
If you use nvm-windows:
|
||||
|
||||
nvm use 24.13.1
|
||||
|
||||
Then run this script again in a new shell.
|
||||
"@
|
||||
}
|
||||
Write-Note "node $raw"
|
||||
}
|
||||
# Bound once so the shared switcher reports through this script's own output
|
||||
# style rather than printing in a voice of its own.
|
||||
$NodeOut = @{ Step = ${function:Write-Step}; Note = ${function:Write-Note} }
|
||||
|
||||
# Reads back only the ids this script wrote. Killing by port would be shorter
|
||||
# and would also kill whatever else happened to be listening.
|
||||
@@ -277,16 +266,27 @@ New-Item -ItemType Directory -Force -Path $StateDir *>$null
|
||||
|
||||
if ($Stop) {
|
||||
Stop-Environment
|
||||
Restore-Node -Version $DefaultNodeVersion @NodeOut
|
||||
return
|
||||
}
|
||||
|
||||
Assert-NodeVersion
|
||||
Use-NodeLatest @NodeOut
|
||||
|
||||
# Anything after the switch reverts on the way out of a failure. Without this a
|
||||
# run that dies in migrations leaves the machine on the new version with nothing
|
||||
# started, and the -Stop that would put it back is never reached.
|
||||
try {
|
||||
Assert-Docker
|
||||
Start-Database
|
||||
Install-IfMissing (Join-Path $RepoRoot 'backend')
|
||||
Install-IfMissing (Join-Path $RepoRoot 'frontend')
|
||||
Start-Backend
|
||||
Start-Frontend
|
||||
}
|
||||
catch {
|
||||
Restore-Node -Version $DefaultNodeVersion @NodeOut
|
||||
throw
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Running.' -ForegroundColor Green
|
||||
@@ -296,5 +296,5 @@ Write-Host " API http://localhost:$ApiPort/api/config"
|
||||
Write-Host " Postgres localhost:$DbPort ($DbUser / $DbPassword / $DbName)"
|
||||
Write-Host ''
|
||||
Write-Host " Logs $StateDir"
|
||||
Write-Host " Stop .\scripts\start-local.ps1 -Stop"
|
||||
Write-Host " Stop .\scripts\start-local.ps1 -Stop (also restores Node $DefaultNodeVersion)"
|
||||
Write-Host ''
|
||||
|
||||
Reference in New Issue
Block a user