The end-to-end helper opens its own connection to read a password-reset token, and nothing guaranteed it pointed at the database the application was actually using. With -E2eDb the app runs on redefined_e2e at 55501 while the helper kept its redefined_local default at 55500, so the app wrote to one database and the suite read another — and the specs failed for a reason that had nothing to do with them. start-local.ps1 now records the coordinates it chose in .local/database.json, and run-tests.ps1 reads them into TEST_PGHOST, TEST_PGPORT, TEST_PGUSER, TEST_PGPASSWORD and TEST_PGDATABASE before Playwright starts. The answer now comes from one place, written by the thing that made the decision at the moment it made it. It is written after Start-Database rather than before, so the file never names a database that failed to come up, and removed by -Stop, so a stopped stack does not leave a record pointing at a container that is gone. Setting all five closes the second fault in the same change. Invoke-IntegrationSuite sets TEST_PGPORT and PowerShell keeps it for the rest of the session, so a -Suite all run leaked the integration port into the e2e run that followed — with none of the matching credentials, leaving the helper offering redefined_local's password to the integration database. Overwriting every one of them is what makes that leak harmless. A missing record throws rather than falling back. A default is what produced both faults in the first place: always plausible, silently wrong, and it fails in ways that look like application bugs rather than configuration. The guard test is the point of the change as much as the fix is. This is the third instance today of two files having to agree with nothing comparing them — #107 and #118 were envValidation against a compose file, #287 was the workflow against start-local.ps1, and this is three files rather than two. The test pins the whole chain: that the writer records the five settings, that the runner reads each of them from the record rather than a default, and that the helper reads no connection variable the runner does not set. Removing a single line from the runner fails four of its assertions, which was checked rather than assumed. What it cannot do is run PowerShell, so these are text assertions against the scripts. That is weaker than executing them and still catches the drift that actually happened. Closes #273 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
9.6 KiB
PowerShell
254 lines
9.6 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 }
|
|
}
|
|
|
|
<#
|
|
.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
|
|
}
|