diff --git a/.gitignore b/.gitignore index faa78aa..66f3eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ artifacts/ !.vscode/launch.json !.vscode/tasks.json node_modules/ +playwright-report/ +test-results/ # User secrets / configs appsettings.Development.json diff --git a/README.md b/README.md index 30ddb58..8f4e35c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,10 @@ Gameplay capabilities now include: - .NET SDK 10.0+ - PowerShell 7+ +- Node.js 22+ - Run `dotnet tool restore` once to enable the repo-local `dotnet-ef` command. +- Run `npm ci` once to install the repo-local Playwright toolchain. +- Run `npm exec playwright install chromium` once to install the browser used by local smoke tests. ## Local Development @@ -84,6 +87,21 @@ Gameplay capabilities now include: ``` 3. Open `http://localhost:5000` (or the port shown in the console). +Playwright helpers: + +- Install/update browser dependencies: + ```powershell + npm exec playwright install chromium + ``` +- Run the checked-in smoke test against an isolated temp SQLite database: + ```powershell + pwsh ./scripts/run-playwright.ps1 + ``` +- Run the Playwright suite directly when the app is already running: + ```powershell + npm run e2e + ``` + VS Code F5 debug profiles are available in `.vscode/launch.json`: - `RpgRoller: Server` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f8bb833 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "rpgroller", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rpgroller", + "devDependencies": { + "@playwright/test": "^1.59.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..42e706c --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "rpgroller", + "private": true, + "scripts": { + "e2e": "playwright test", + "e2e:smoke": "playwright test tests/e2e/smoke.spec.js --reporter=line", + "e2e:install": "playwright install chromium" + }, + "devDependencies": { + "@playwright/test": "^1.59.1" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..ddd2abf --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,13 @@ +const { defineConfig } = require("@playwright/test"); + +module.exports = defineConfig({ + testDir: "./tests/e2e", + timeout: 30_000, + fullyParallel: false, + reporter: "line", + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:5000", + headless: true, + trace: "retain-on-failure" + } +}); diff --git a/scripts/ci-local.ps1 b/scripts/ci-local.ps1 index 7b353ee..4ee307f 100644 --- a/scripts/ci-local.ps1 +++ b/scripts/ci-local.ps1 @@ -1,6 +1,7 @@ param( [switch]$SkipDotnetRestore, - [switch]$SkipBuild + [switch]$SkipBuild, + [switch]$SkipPlaywright ) Set-StrictMode -Version Latest @@ -36,6 +37,14 @@ try { } } + Invoke-Step -Name "Restore Node dependencies" -Action { + npm ci + } + + Invoke-Step -Name "Ensure Playwright browser" -Action { + npm exec playwright install chromium + } + Invoke-Step -Name "Run tests" -Action { if ($SkipBuild) { dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --verbosity normal --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings @@ -49,6 +58,12 @@ try { pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 } + if (-not $SkipPlaywright) { + Invoke-Step -Name "Run Playwright smoke test" -Action { + pwsh ./scripts/run-playwright.ps1 + } + } + Write-Host "CI checks passed." } finally { diff --git a/scripts/run-playwright.ps1 b/scripts/run-playwright.ps1 new file mode 100644 index 0000000..207f85e --- /dev/null +++ b/scripts/run-playwright.ps1 @@ -0,0 +1,61 @@ +param( + [string]$BaseUrl = "http://127.0.0.1:5095", + [string]$Spec = "tests/e2e/smoke.spec.js" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$appUrl = [Uri]$BaseUrl +$healthUrl = "$BaseUrl/api/health" +$tempDbPath = Join-Path $env:TEMP ("rpgroller-playwright-{0}.db" -f [Guid]::NewGuid().ToString("N")) +$process = $null + +Push-Location $repoRoot +try { + $env:ConnectionStrings__RpgRoller = "Data Source=$tempDbPath" + $env:PLAYWRIGHT_BASE_URL = $BaseUrl + + $process = Start-Process dotnet -ArgumentList @( + "run", + "--project", + "RpgRoller/RpgRoller.csproj", + "--urls", + $BaseUrl + ) -WorkingDirectory $repoRoot -PassThru + + $response = $null + for ($i = 0; $i -lt 60; $i++) { + try { + $response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 2 + if ($response.StatusCode -eq 200) { + break + } + } + catch { + Start-Sleep -Milliseconds 500 + } + + Start-Sleep -Milliseconds 500 + } + + if (-not $response -or $response.StatusCode -ne 200) { + throw "Application failed to start on $BaseUrl." + } + + npm exec playwright test $Spec -- --reporter=line + if ($LASTEXITCODE -ne 0) { + throw "Playwright exited with code $LASTEXITCODE." + } +} +finally { + if ($process -and -not $process.HasExited) { + Stop-Process -Id $process.Id -Force + } + + Remove-Item Env:\ConnectionStrings__RpgRoller -ErrorAction SilentlyContinue + Remove-Item Env:\PLAYWRIGHT_BASE_URL -ErrorAction SilentlyContinue + Pop-Location +} diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js new file mode 100644 index 0000000..900c0af --- /dev/null +++ b/tests/e2e/smoke.spec.js @@ -0,0 +1,11 @@ +const { test, expect } = require("@playwright/test"); + +test("home page loads auth entry points", async ({ page }) => { + await page.goto("/"); + + await expect(page.locator("h1")).toContainText("RpgRoller"); + await expect(page.getByRole("heading", { name: "Register" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + await expect(page.getByLabel("Username").first()).toBeVisible(); + await expect(page.getByLabel("Password").nth(1)).toBeVisible(); +});