Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1d4f21e
Add apps/e2e Playwright package scaffold
batzionb Jul 27, 2026
fc4d3cb
Add automated Keycloak login for e2e auth setup
batzionb Jul 27, 2026
6f7d768
Add authenticated smoke test
batzionb Jul 27, 2026
5731f15
Ignore e2e auth state and test output
batzionb Jul 27, 2026
cd50327
Add apps/e2e README
batzionb Jul 27, 2026
ee45061
Fix e2e auth against a real cluster: TLS trust and two-step login
batzionb Jul 27, 2026
c177219
Add e2e:dev script for testing against the local dev server
batzionb Jul 27, 2026
525c1b4
Stop persisting e2e test specs — harness only, tests are throwaway
batzionb Jul 27, 2026
4d92062
Clarify e2e harness docs: throwaway specs, not UI CI coverage
batzionb Jul 27, 2026
181a270
fix: address CodeRabbit review feedback on e2e harness
batzionb Jul 27, 2026
2fbc0a3
Rename e2e Playwright harness to apps/playwright to avoid confusion w…
batzionb Jul 28, 2026
422e6fc
Simplify apps/playwright wording in AGENTS.md
batzionb Jul 28, 2026
79ef782
fix: address round 2 of CodeRabbit review feedback on Playwright harness
batzionb Jul 29, 2026
89e1807
fix: add smoke spec and drop redundant playwright:dev script
batzionb Aug 2, 2026
9961980
fix: address round 3 of CodeRabbit review feedback on Playwright harness
batzionb Aug 2, 2026
58878e0
fix: remove remaining scratch-spec cleanup contradiction in README
batzionb Aug 2, 2026
1241214
docs: drop osac-test-infra reference from playwright README
batzionb Aug 2, 2026
51a2aad
fix: harden .auth directory before storageState writes session cookie
batzionb Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,19 @@ proxy/osac-proxy
# AI workflow artifacts
.artifacts/
/.claude/

# apps/playwright — manual browser verification against a live cluster, not
# an e2e test suite. Live session state and run output, never committed.
apps/playwright/.auth/
apps/playwright/test-results/
apps/playwright/playwright-report/
apps/playwright/blob-report/

# apps/playwright — ad hoc specs are written here for manual verification and
# are never committed; only the harness itself is.
apps/playwright/scratch/

# apps/playwright — ad hoc specs sometimes call page.screenshot() with a bare
# filename, which lands in apps/playwright/ instead of a gitignored output dir.
apps/playwright/*.png
apps/playwright/*.jpg
21 changes: 20 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Multi-stage build images: `nodejs-22-minimal:9.8`, `go-toolset:1.25`, `ubi-minim
| `@osac/ui-components` | Shared components consumed at source (no build) — typed gRPC hooks live here |
| `@osac/types` | Generated protobuf types and service descriptors — **never edit**, regenerate with `pnpm gen-types` |
| `@osac/i18n` | Translation extraction — `locales/en/translation.json` is generated, not hand-edited |
| `@osac/playwright` | Playwright harness for manual verification against a **live deployed cluster** — not a CI suite, not persisted test coverage. See [Manual verification against a live cluster](#manual-verification-against-a-live-cluster) |

## Code Style

Expand Down Expand Up @@ -179,9 +180,27 @@ export const getLabels = (t: TFunction) => ({
- `apps/app-frontend/vitest.config.ts` — single runner for app-frontend and ui-components tests (`include` spans both packages)
- ESLint relaxes type safety rules for test files (no-unsafe-* off)
- Testing libraries: @testing-library/react 16.x, @testing-library/jest-dom 6.x
- No E2E tests in this repoE2E coverage lives in `osac-test-infra`
- No persisted E2E test suite in this repo, and no persisted UI-level E2E coverage anywhere: `osac-test-infra`'s CI-tracked E2E suites hit the fulfillment gRPC/REST API and Kubernetes CRs directly — no browser, nothing UI-related
- CI runs lint, test, and container build; run `pnpm test` locally before submitting

### Manual verification against a live cluster

`apps/playwright` (`@osac/playwright`) is a Playwright harness for manually confirming
that a change works end-to-end against a **live, already-deployed** cluster —
not a CI suite and not something that adds to this repo's persisted test count. See
[apps/playwright/README.md](apps/playwright/README.md).

- **This is not a test suite — specs are throwaway**: write ad hoc specs under
`apps/playwright/scratch/`, a gitignored directory that only exists locally.
`pnpm playwright` runs every spec under it. Nothing there is ever committed
or needs manual cleanup — only the harness itself (`playwright.config.ts`,
`auth.setup.ts`) is committed. Never write specs under `apps/playwright/src/`,
and never treat a `scratch/` spec as regression coverage.
- Use this when asked to verify a UI change actually works in a browser against
a real deployment — requires `OSAC_UI_BASE_URL`, `OSAC_USERNAME`, `OSAC_PASSWORD`
(or `pnpm playwright:dev` against a local `pnpm dev` instance). There is no
mock `fulfillment-service`, so this cannot run hermetically or in CI.

## Build

**Frontend build** (apps/app-frontend):
Expand Down
130 changes: 130 additions & 0 deletions apps/playwright/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# @osac/playwright

Playwright browser tests for manually verifying `osac-ui` against a **live,
already-deployed** cluster — the running proxy, SPA, Keycloak realm, and
`fulfillment-service` backend all in one real environment.

This is not a CI suite. There is no mock server for `fulfillment-service`
and no way to run this hermetically — it exists for AI agents to manually confirm a
change actually works end-to-end against a real deployment, the way you'd
otherwise do by hand in a browser.

## Prerequisites

- A reachable `osac-ui` deployment (any HTTPS URL that serves it).
- A Keycloak user on that deployment's realm to log in as. This package
doesn't provision one — use an existing test account, or create one on the
target environment first.
- Playwright's browser binaries installed once per machine (see Setup below).

## Setup

```bash
pnpm install
pnpm --filter @osac/playwright exec playwright install chromium
```

## Running

Required environment variables:

| Variable | Description |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| `OSAC_UI_BASE_URL` | Full URL of the `osac-ui` deployment to test, e.g. `https://osac-ui-<namespace>.<cluster-domain>` |
| `OSAC_USERNAME` | Username of a Keycloak test user on that deployment's realm |
| `OSAC_PASSWORD` | Password for that user |

```bash
export OSAC_UI_BASE_URL=https://... OSAC_USERNAME=...
read -rs OSAC_PASSWORD && export OSAC_PASSWORD # avoids leaving the password in shell history
pnpm playwright
```

(`pnpm playwright` is a root-level shortcut for `pnpm --filter @osac/playwright
run playwright`.) None of these have defaults — each is required, so a missing
value fails immediately with a clear error instead of silently pointing at the
wrong environment. `pnpm playwright` runs every spec under `scratch/` — see
[Writing a test](#writing-a-test).

Optional:

| Variable | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `IGNORE_HTTPS_ERRORS` | Set to `true` to trust self-signed/cluster-internal CA certs, common on dev and lab clusters. Off by default because this flow submits a real Keycloak password — only enable it against clusters you trust. `pnpm playwright:dev` sets this for you (see below). |

Testing against a local `pnpm dev` instance instead of a remote deployment:

```bash
export OSAC_USERNAME=...
read -rs OSAC_PASSWORD && export OSAC_PASSWORD
pnpm playwright:dev
```

`pnpm playwright:dev` sets `OSAC_UI_BASE_URL=http://localhost:5173` and
`IGNORE_HTTPS_ERRORS=true` for you — the backing `fulfillment-service`/
Keycloak for a local dev server is typically a dev/lab cluster with a
self-signed or cluster-internal CA cert. Still requires `pnpm dev` running in
another terminal, and still needs `OSAC_USERNAME`/`OSAC_PASSWORD`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## How authentication works

`osac-ui` has no test-mode auth bypass — every login goes through a real
Keycloak realm via the app's normal OIDC flow. Rather than repeating that
flow (and Keycloak's own login page) for every test, this harness follows
Playwright's standard pattern for testing authenticated apps:

1. A `setup` project (`src/auth.setup.ts`) runs once per test run. It opens
the app, which auto-redirects to Keycloak when unauthenticated, fills in
`OSAC_USERNAME`/`OSAC_PASSWORD` on Keycloak's real login form, and saves the
resulting browser session (cookies) to `.auth/user.json`.
2. Every real test (the `chromium` project) declares a dependency on `setup`
and loads that saved session before the test body runs — so tests start
already logged in, and the login flow only ever runs once per invocation.

`.auth/user.json` contains a live, real session (the actual `osac-access`
cookie) — it's gitignored and must never be committed or shared. It's
regenerated fresh every time you run `pnpm playwright`.

If the target realm's login page uses a custom Keycloak theme, the field/
button selectors in `auth.setup.ts` (written against Keycloak's default
theme) may need adjusting.

## Writing a test

**This is not a test suite.** This package provides the harness (auth +
config) only. Test specs are throwaway, written ad hoc for a specific
manual-verification task (typically by an AI agent working through a change)
and run once, then discarded.

Specs live under `scratch/` — a gitignored directory that only exists on your
machine. Nothing written there is ever committed, so there's nothing to clean
up and no risk of it landing in a PR. Do not write specs under `src/`, and do
not treat anything under `scratch/` as regression coverage — if a change
needs persisted UI coverage, that's a job for `osac-test-infra`'s gRPC-level
suite, not this harness.

```bash
mkdir -p apps/playwright/scratch
cat > apps/playwright/scratch/check.spec.ts <<'EOF'
import { expect, test } from '@playwright/test';

test('loads the authenticated dashboard', async ({ page }) => {
await page.goto('/');

await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible();
});
EOF
```

It runs in the `chromium` project — already authenticated via `auth.setup.ts`,
no need to handle login in the test itself. Run it with:

```bash
export OSAC_UI_BASE_URL=... OSAC_USERNAME=...
read -rs OSAC_PASSWORD && export OSAC_PASSWORD
pnpm playwright
```

`pnpm playwright` runs every spec under `scratch/` — delete the file when you're done
with it (or leave it; it's gitignored either way, but a stale spec will run
again next time).
15 changes: 15 additions & 0 deletions apps/playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@osac/playwright",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"playwright": "playwright test",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@playwright/test": "^1.62.0",
"@types/node": "^20.19.41",
"typescript": "~5.9.3"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
72 changes: 72 additions & 0 deletions apps/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { defineConfig, devices } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { AUTH_FILE } from './src/auth-file';

const baseURL = process.env.OSAC_UI_BASE_URL;
if (!baseURL) {
throw new Error('OSAC_UI_BASE_URL must be set to the URL of a running osac-ui instance.');
}
// Diagnostics below never interpolate the raw value — a malformed OSAC_UI_BASE_URL
// can carry embedded credentials or an internal hostname that shouldn't land
// in CI logs.
const invalidBaseURLError = new Error(
'OSAC_UI_BASE_URL must be an absolute http(s) URL with no embedded username/password.',
);
let parsedBaseURL: URL;
try {
parsedBaseURL = new URL(baseURL);
} catch {
throw invalidBaseURLError;
}
if (parsedBaseURL.protocol !== 'http:' && parsedBaseURL.protocol !== 'https:') {
throw invalidBaseURLError;
}
if (parsedBaseURL.username || parsedBaseURL.password) {
throw invalidBaseURLError;
}

// Ad hoc specs (typically written by an AI agent to manually verify a change)
// live in this gitignored scratch directory, never under src/ — that keeps
// the "throwaway, not a test suite" boundary obvious to anyone browsing the
// package, instead of relying on everyone remembering src/*.spec.ts is ignored.
const scratchDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'scratch');
fs.mkdirSync(scratchDir, { recursive: true });

export default defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
reporter: 'html',
expect: {
timeout: 15_000,
},
use: {
baseURL,
trace: 'on-first-retry',
// Dev/lab cluster routes commonly present a self-signed or cluster-internal
// CA cert that Chromium doesn't trust by default (the same reason manual
// testing against these environments always needs curl -k). Opt-in only —
// this flow submits a real Keycloak password, so TLS verification stays on
// by default.
ignoreHTTPSErrors: process.env.IGNORE_HTTPS_ERRORS === 'true',
},
projects: [
{
name: 'setup',
testDir: './src',
testMatch: /auth\.setup\.ts/,
// Traces record fill() arguments — never trace the project that types the
// real Keycloak password, even if a future CI run retries it.
use: { ...devices['Desktop Chrome'], trace: 'off' },
},
{
name: 'chromium',
testDir: scratchDir,
use: { ...devices['Desktop Chrome'], storageState: AUTH_FILE },
dependencies: ['setup'],
},
],
});
1 change: 1 addition & 0 deletions apps/playwright/src/auth-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const AUTH_FILE = '.auth/user.json';
31 changes: 31 additions & 0 deletions apps/playwright/src/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test as setup } from '@playwright/test';

import { AUTH_FILE } from './auth-file';

setup('authenticate', async ({ page }) => {
const username = process.env.OSAC_USERNAME;
const password = process.env.OSAC_PASSWORD;
if (!username || !password) {
throw new Error('OSAC_USERNAME and OSAC_PASSWORD must be set to a valid Keycloak test user.');
}

// The app checks /api/login/info on load and, if unauthenticated, immediately
// redirects the browser to Keycloak itself (apps/app-frontend/src/hooks/oidc-login.tsx)
// — there is no login button to click first. This realm's theme is a two-step
// identifier-first flow: username + "Sign In" submits to a second screen with
// the password field, then the same "Sign In" label submits that too. Field/
// button names match Keycloak's default theme; a custom theme may differ.
await page.goto('/');
await page.getByLabel('Username or email').fill(username);
await page.getByRole('button', { name: 'Sign In' }).click();
// getByLabel('Password') is ambiguous — it also matches the theme's "Show
// password" toggle button, which shares the same label association. A
// native input[type=password] has no ARIA role, so getByRole('textbox')
// won't match it either.
await page.locator('input[type="password"]').fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible();

await page.context().storageState({ path: AUTH_FILE });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
4 changes: 4 additions & 0 deletions apps/playwright/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src", "playwright.config.ts"]
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
"lint": "eslint \"{apps,libs}/*/src/**\" && prettier --check \"{apps,libs}/*/src/**/*.{js,jsx,ts,tsx}\" && pnpm run i18n",
"format": "eslint --fix \"{apps,libs}/*/src/**/*.{ts,tsx}\" && prettier --write \"{apps,libs}/*/src/**/*.{js,jsx,ts,tsx}\"",
"test": "pnpm -r run test",
"typecheck": "pnpm -r run typecheck"
"typecheck": "pnpm -r run typecheck",
"playwright": "pnpm --filter @osac/playwright run playwright",
"playwright:dev": "OSAC_UI_BASE_URL=http://localhost:5173 IGNORE_HTTPS_ERRORS=true pnpm playwright",
"playwright:report": "pnpm --filter @osac/playwright exec playwright show-report"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
Expand Down
Loading
Loading