Add Playwright E2E testing infrastructure
- Add playwright-web-flake to flake.nix for NixOS browser support - Pin @playwright/test@1.56.1 to match nixpkgs version - Create playwright.config.ts with Chromium-only, auto-start dev server - Add e2e/smoke.spec.ts with initial smoke tests - Add .mcp.json for Claude browser control via MCP - Update .gitignore for playwright artifacts - Remove E2E test skip from spec.md Known Limitations - Update specs/testing.md to require three-tier testing approach Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,6 +13,10 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# playwright
|
||||||
|
/playwright-report/
|
||||||
|
/test-results/
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|||||||
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@playwright/mcp@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
e2e/smoke.spec.ts
Normal file
36
e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// ABOUTME: Smoke tests to verify basic application functionality.
|
||||||
|
// ABOUTME: Tests that the app loads and critical pages are accessible.
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("smoke tests", () => {
|
||||||
|
test("app loads with correct title", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Verify the app loads by checking the page title
|
||||||
|
await expect(page).toHaveTitle("PhaseFlow");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login page is accessible", async ({ page }) => {
|
||||||
|
await page.goto("/login");
|
||||||
|
|
||||||
|
// Verify login page loads
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unauthenticated root redirects or shows login option", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// The app should either redirect to login or show a login link
|
||||||
|
// Check for either condition
|
||||||
|
const url = page.url();
|
||||||
|
const hasLoginInUrl = url.includes("/login");
|
||||||
|
const loginLink = page.getByRole("link", { name: /login|sign in/i });
|
||||||
|
|
||||||
|
// At least one should be true: either we're on login page or there's a login link
|
||||||
|
if (!hasLoginInUrl) {
|
||||||
|
await expect(loginLink).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
68
flake.lock
generated
68
flake.lock
generated
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1726560853,
|
||||||
|
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767892417,
|
"lastModified": 1767892417,
|
||||||
@@ -16,9 +34,57 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 0,
|
||||||
|
"narHash": "sha256-u+rxA79a0lyhG+u+oPBRtTDtzz8kvkc9a6SWSt9ekVc=",
|
||||||
|
"path": "/nix/store/0283cbhm47kd3lr9zmc5fvdrx9qkav8s-source",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playwright-web-flake": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1764622772,
|
||||||
|
"narHash": "sha256-WCvvlB9sH6u8MQUkFnlxx7jDh7kIebTDK/JHi6pPqSA=",
|
||||||
|
"owner": "pietdevries94",
|
||||||
|
"repo": "playwright-web-flake",
|
||||||
|
"rev": "88e0e6c69b9086619b0c4d8713b2bfaf81a21c40",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pietdevries94",
|
||||||
|
"ref": "1.56.1",
|
||||||
|
"repo": "playwright-web-flake",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"playwright-web-flake": "playwright-web-flake"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
30
flake.nix
30
flake.nix
@@ -2,11 +2,13 @@
|
|||||||
# ABOUTME: Provides Node.js 24, pnpm, turbo, lefthook, and Docker image output.
|
# ABOUTME: Provides Node.js 24, pnpm, turbo, lefthook, and Docker image output.
|
||||||
{
|
{
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
inputs.playwright-web-flake.url = "github:pietdevries94/playwright-web-flake/1.56.1";
|
||||||
|
|
||||||
outputs = { nixpkgs, ... }:
|
outputs = { nixpkgs, playwright-web-flake, ... }:
|
||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
playwright-driver = playwright-web-flake.packages.${system}.playwright-driver;
|
||||||
|
|
||||||
# Custom Python package: garth (not in nixpkgs)
|
# Custom Python package: garth (not in nixpkgs)
|
||||||
garth = pkgs.python3Packages.buildPythonPackage {
|
garth = pkgs.python3Packages.buildPythonPackage {
|
||||||
@@ -48,32 +50,40 @@
|
|||||||
devShells.${system} = {
|
devShells.${system} = {
|
||||||
# Default development shell with all tools
|
# Default development shell with all tools
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = commonPackages ++ (with pkgs; [
|
packages = commonPackages ++ [
|
||||||
turbo
|
pkgs.turbo
|
||||||
lefthook
|
pkgs.lefthook
|
||||||
]);
|
playwright-driver
|
||||||
|
];
|
||||||
|
|
||||||
# For native modules (sharp, better-sqlite3, etc.)
|
# For native modules (sharp, better-sqlite3, etc.)
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
||||||
|
|
||||||
|
# Playwright browser configuration for NixOS (from playwright-web-flake)
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers}";
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Ralph sandbox shell with minimal permissions
|
# Ralph sandbox shell with minimal permissions
|
||||||
# Used for autonomous Ralph loop execution
|
# Used for autonomous Ralph loop execution
|
||||||
ralph = pkgs.mkShell {
|
ralph = pkgs.mkShell {
|
||||||
packages = commonPackages ++ (with pkgs; [
|
packages = commonPackages ++ [
|
||||||
# Claude CLI (assumes installed globally or via npm)
|
playwright-driver
|
||||||
# Add any other tools Ralph needs here
|
];
|
||||||
]);
|
|
||||||
|
|
||||||
# Restrictive environment for sandboxed execution
|
# Restrictive environment for sandboxed execution
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
echo "🔒 Ralph Sandbox Environment"
|
echo "🔒 Ralph Sandbox Environment"
|
||||||
echo " Limited to: nodejs, pnpm, git"
|
echo " Limited to: nodejs, pnpm, git, playwright"
|
||||||
echo ""
|
echo ""
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# For native modules
|
# For native modules
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
||||||
|
|
||||||
|
# Playwright browser configuration for NixOS (from playwright-web-flake)
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers}";
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:headed": "playwright test --headed",
|
||||||
"db:setup": "npx tsx scripts/setup-db.ts"
|
"db:setup": "npx tsx scripts/setup-db.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.3.11",
|
"@biomejs/biome": "2.3.11",
|
||||||
|
"@playwright/test": "1.56.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
|||||||
46
playwright.config.ts
Normal file
46
playwright.config.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// ABOUTME: Playwright E2E test configuration for browser-based testing.
|
||||||
|
// ABOUTME: Configures Chromium-only headless testing with automatic dev server startup.
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Test directory for E2E tests
|
||||||
|
testDir: "./e2e",
|
||||||
|
|
||||||
|
// Run tests in parallel
|
||||||
|
fullyParallel: true,
|
||||||
|
|
||||||
|
// Fail the build on CI if you accidentally left test.only in the source code
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
||||||
|
// Retry failed tests on CI only
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|
||||||
|
// Limit parallel workers on CI to avoid resource issues
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
|
||||||
|
// Reporter configuration
|
||||||
|
reporter: [["html", { open: "never" }], ["list"]],
|
||||||
|
|
||||||
|
// Shared settings for all projects
|
||||||
|
use: {
|
||||||
|
// Base URL for navigation actions like page.goto('/')
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
|
||||||
|
// Collect trace on first retry for debugging
|
||||||
|
trace: "on-first-retry",
|
||||||
|
|
||||||
|
// Take screenshot on failure
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configure projects - Chromium only per requirements
|
||||||
|
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
|
||||||
|
|
||||||
|
// Run dev server before starting tests
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm dev",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120 * 1000, // 2 minutes for Next.js to start
|
||||||
|
},
|
||||||
|
});
|
||||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -25,7 +25,7 @@ importers:
|
|||||||
version: 0.562.0(react@19.2.3)
|
version: 0.562.0(react@19.2.3)
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.1
|
specifier: 16.1.1
|
||||||
version: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
node-cron:
|
node-cron:
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
@@ -57,6 +57,9 @@ importers:
|
|||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 2.3.11
|
specifier: 2.3.11
|
||||||
version: 2.3.11
|
version: 2.3.11
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: 1.56.1
|
||||||
|
version: 1.56.1
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
@@ -974,6 +977,11 @@ packages:
|
|||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
|
'@playwright/test@1.56.1':
|
||||||
|
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.53':
|
'@rolldown/pluginutils@1.0.0-beta.53':
|
||||||
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
||||||
|
|
||||||
@@ -1553,6 +1561,11 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -1778,6 +1791,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g==}
|
resolution: {integrity: sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
playwright-core@1.56.1:
|
||||||
|
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.56.1:
|
||||||
|
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pocketbase@0.26.5:
|
pocketbase@0.26.5:
|
||||||
resolution: {integrity: sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ==}
|
resolution: {integrity: sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ==}
|
||||||
|
|
||||||
@@ -2723,6 +2746,10 @@ snapshots:
|
|||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
|
'@playwright/test@1.56.1':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.56.1
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.55.1':
|
'@rollup/rollup-android-arm-eabi@4.55.1':
|
||||||
@@ -3213,6 +3240,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3363,7 +3393,7 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.1
|
'@next/env': 16.1.1
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -3383,6 +3413,7 @@ snapshots:
|
|||||||
'@next/swc-win32-arm64-msvc': 16.1.1
|
'@next/swc-win32-arm64-msvc': 16.1.1
|
||||||
'@next/swc-win32-x64-msvc': 16.1.1
|
'@next/swc-win32-x64-msvc': 16.1.1
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@playwright/test': 1.56.1
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -3426,6 +3457,14 @@ snapshots:
|
|||||||
sonic-boom: 4.2.0
|
sonic-boom: 4.2.0
|
||||||
thread-stream: 4.0.0
|
thread-stream: 4.0.0
|
||||||
|
|
||||||
|
playwright-core@1.56.1: {}
|
||||||
|
|
||||||
|
playwright@1.56.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.56.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
pocketbase@0.26.5: {}
|
pocketbase@0.26.5: {}
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
|
|||||||
2
spec.md
2
spec.md
@@ -835,7 +835,7 @@ The following are **out of scope** for MVP:
|
|||||||
| Hormonal birth control | May disrupt natural cycle phases |
|
| Hormonal birth control | May disrupt natural cycle phases |
|
||||||
| API versioning | Single version; breaking changes via deprecation |
|
| API versioning | Single version; breaking changes via deprecation |
|
||||||
| Formal API documentation | Endpoints documented in spec only |
|
| Formal API documentation | Endpoints documented in spec only |
|
||||||
| E2E tests | Unit + integration tests only (authorized skip) |
|
| Multi-user support | Single-user design only |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
130
specs/testing.md
130
specs/testing.md
@@ -6,7 +6,7 @@ When I make changes to the codebase, I want automated tests to catch regressions
|
|||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
PhaseFlow uses **unit and integration tests** with Vitest. End-to-end tests are not required for MVP (authorized skip).
|
PhaseFlow uses a **three-tier testing approach**: unit tests, integration tests, and end-to-end tests.
|
||||||
|
|
||||||
### Test Types
|
### Test Types
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ PhaseFlow uses **unit and integration tests** with Vitest. End-to-end tests are
|
|||||||
|------|-------|-------|----------|
|
|------|-------|-------|----------|
|
||||||
| Unit | Pure functions, utilities | Vitest | Colocated `*.test.ts` |
|
| Unit | Pure functions, utilities | Vitest | Colocated `*.test.ts` |
|
||||||
| Integration | API routes, PocketBase interactions | Vitest + supertest | Colocated `*.test.ts` |
|
| Integration | API routes, PocketBase interactions | Vitest + supertest | Colocated `*.test.ts` |
|
||||||
|
| E2E | Full user flows, browser interactions | Playwright | `e2e/*.spec.ts` |
|
||||||
|
|
||||||
## Framework
|
## Framework
|
||||||
|
|
||||||
@@ -148,9 +149,96 @@ describe('GET /api/today', () => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## End-to-End Tests
|
||||||
|
|
||||||
|
Test complete user flows through the browser using Playwright.
|
||||||
|
|
||||||
|
### Framework
|
||||||
|
|
||||||
|
**Playwright** - Cross-browser E2E testing with auto-waiting and tracing.
|
||||||
|
|
||||||
|
**Configuration (`playwright.config.ts`):**
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
reporter: [["html", { open: "never" }], ["list"]],
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm dev",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority Targets
|
||||||
|
|
||||||
|
| Flow | Tests |
|
||||||
|
|------|-------|
|
||||||
|
| Authentication | Login page loads, login redirects, logout works |
|
||||||
|
| Dashboard | Shows decision, displays cycle info, override toggles work |
|
||||||
|
| Settings | Garmin token paste, preferences save |
|
||||||
|
| Calendar | ICS feed accessible, calendar view renders |
|
||||||
|
| Period logging | "Period Started" updates cycle |
|
||||||
|
|
||||||
|
### Example Test
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// e2e/dashboard.spec.ts
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("dashboard", () => {
|
||||||
|
test("shows training decision for authenticated user", async ({ page }) => {
|
||||||
|
// Login first (or use auth state)
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Verify decision card is visible
|
||||||
|
await expect(page.getByTestId("decision-card")).toBeVisible();
|
||||||
|
|
||||||
|
// Verify cycle day is displayed
|
||||||
|
await expect(page.getByText(/Day \d+ of \d+/)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("override toggles update decision", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Enable flare mode
|
||||||
|
await page.getByLabel("Flare Mode").click();
|
||||||
|
|
||||||
|
// Decision should change to gentle
|
||||||
|
await expect(page.getByText(/GENTLE/)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### NixOS Setup
|
||||||
|
|
||||||
|
E2E tests require browser binaries. On NixOS, use `playwright-web-flake`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# In flake.nix inputs
|
||||||
|
inputs.playwright-web-flake.url = "github:pietdevries94/playwright-web-flake/1.56.1";
|
||||||
|
|
||||||
|
# In devShell
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers}";
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
|
||||||
|
```
|
||||||
|
|
||||||
## File Naming
|
## File Naming
|
||||||
|
|
||||||
Tests colocated with source files:
|
Tests colocated with source files (unit/integration) or in `e2e/` directory (E2E):
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
@@ -164,22 +252,33 @@ src/
|
|||||||
today/
|
today/
|
||||||
route.ts
|
route.ts
|
||||||
route.test.ts
|
route.test.ts
|
||||||
|
e2e/
|
||||||
|
smoke.spec.ts
|
||||||
|
dashboard.spec.ts
|
||||||
|
auth.spec.ts
|
||||||
|
settings.spec.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run unit/integration tests
|
||||||
npm test
|
pnpm test:run
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
npm run test:coverage
|
|
||||||
|
|
||||||
# Run in watch mode
|
# Run in watch mode
|
||||||
npm run test:watch
|
pnpm test
|
||||||
|
|
||||||
# Run specific file
|
# Run E2E tests (headless)
|
||||||
npm test -- src/lib/cycle.test.ts
|
pnpm test:e2e
|
||||||
|
|
||||||
|
# Run E2E tests (visible browser)
|
||||||
|
pnpm test:e2e:headed
|
||||||
|
|
||||||
|
# Run E2E tests with UI mode
|
||||||
|
pnpm test:e2e:ui
|
||||||
|
|
||||||
|
# Run all tests (unit + E2E)
|
||||||
|
pnpm test:run && pnpm test:e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
## Coverage Expectations
|
## Coverage Expectations
|
||||||
@@ -190,15 +289,20 @@ No strict coverage thresholds for MVP, but aim for:
|
|||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
1. All tests pass in CI before merge
|
1. All tests (unit, integration, E2E) pass in CI before merge
|
||||||
2. Core decision engine logic has comprehensive tests
|
2. Core decision engine logic has comprehensive unit tests
|
||||||
3. Phase scaling tested for multiple cycle lengths
|
3. Phase scaling tested for multiple cycle lengths
|
||||||
4. API auth tested for protected routes
|
4. API auth tested for protected routes
|
||||||
|
5. Critical user flows covered by E2E tests
|
||||||
|
|
||||||
## Acceptance Tests
|
## Acceptance Tests
|
||||||
|
|
||||||
- [ ] `npm test` runs without errors
|
- [ ] `pnpm test:run` runs without errors
|
||||||
|
- [ ] `pnpm test:e2e` runs without errors
|
||||||
- [ ] Unit tests cover decision engine logic
|
- [ ] Unit tests cover decision engine logic
|
||||||
- [ ] Unit tests cover cycle phase calculations
|
- [ ] Unit tests cover cycle phase calculations
|
||||||
- [ ] Integration tests verify API authentication
|
- [ ] Integration tests verify API authentication
|
||||||
|
- [ ] E2E tests verify login flow
|
||||||
|
- [ ] E2E tests verify dashboard displays correctly
|
||||||
|
- [ ] E2E tests verify period logging works
|
||||||
- [ ] Tests run in CI pipeline
|
- [ ] Tests run in CI pipeline
|
||||||
|
|||||||
Reference in New Issue
Block a user