-
Notifications
You must be signed in to change notification settings - Fork 65
NO-ISSUE: add Playwright e2e harness for manual verification against a live cluster #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
openshift-merge-bot
merged 18 commits into
osac-project:main
from
batzionb:feature/e2e-playwright-setup
Aug 3, 2026
Merged
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
1d4f21e
Add apps/e2e Playwright package scaffold
batzionb fc4d3cb
Add automated Keycloak login for e2e auth setup
batzionb 6f7d768
Add authenticated smoke test
batzionb 5731f15
Ignore e2e auth state and test output
batzionb cd50327
Add apps/e2e README
batzionb ee45061
Fix e2e auth against a real cluster: TLS trust and two-step login
batzionb c177219
Add e2e:dev script for testing against the local dev server
batzionb 525c1b4
Stop persisting e2e test specs — harness only, tests are throwaway
batzionb 4d92062
Clarify e2e harness docs: throwaway specs, not UI CI coverage
batzionb 181a270
fix: address CodeRabbit review feedback on e2e harness
batzionb 2fbc0a3
Rename e2e Playwright harness to apps/playwright to avoid confusion w…
batzionb 422e6fc
Simplify apps/playwright wording in AGENTS.md
batzionb 79ef782
fix: address round 2 of CodeRabbit review feedback on Playwright harness
batzionb 89e1807
fix: add smoke spec and drop redundant playwright:dev script
batzionb 9961980
fix: address round 3 of CodeRabbit review feedback on Playwright harness
batzionb 58878e0
fix: remove remaining scratch-spec cleanup contradiction in README
batzionb 1241214
docs: drop osac-test-infra reference from playwright README
batzionb 51a2aad
fix: harden .auth directory before storageState writes session cookie
batzionb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| # @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` logs in, runs the [smoke | ||
| test](#smoke-test), and runs every spec under `scratch/` — see [Writing a | ||
| test](#writing-a-test). | ||
|
|
||
| For iterating on a `scratch/` spec, logging in on every run is slow and adds a | ||
| real Keycloak request each time. Log in once, then re-run just the spec as | ||
| many times as you need, reusing the saved session: | ||
|
|
||
| ```bash | ||
| pnpm playwright:setup # logs in once, writes .auth/user.json | ||
| pnpm playwright:run # runs scratch/ specs only, no login | ||
| pnpm playwright:run # ...repeat as many times as you're iterating | ||
| ``` | ||
|
|
||
| `pnpm playwright:run` never re-authenticates — if `.auth/user.json` is missing | ||
| or the session has expired, re-run `pnpm playwright:setup` first. | ||
|
|
||
| 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 — it disables TLS verification for the whole browser context, not just `localhost`, so only enable it against clusters you trust. | | ||
|
|
||
| Testing against a local `pnpm dev` instance instead of a remote deployment: | ||
| requires `pnpm dev` running in another terminal, and still needs | ||
| `OSAC_USERNAME`/`OSAC_PASSWORD`. | ||
|
|
||
| ```bash | ||
| export OSAC_UI_BASE_URL=http://localhost:5173 OSAC_USERNAME=... | ||
| read -rs OSAC_PASSWORD && export OSAC_PASSWORD | ||
| pnpm playwright | ||
| ``` | ||
|
|
||
| The backing `fulfillment-service`/Keycloak for a local dev server is often a | ||
| dev/lab cluster with a self-signed or cluster-internal CA cert — the real | ||
| login redirect goes there even though the UI itself is on `localhost`, so if | ||
| you hit a TLS error, explicitly opt in with `IGNORE_HTTPS_ERRORS=true` rather | ||
| than trusting it by default: | ||
|
|
||
| ```bash | ||
| export OSAC_UI_BASE_URL=http://localhost:5173 OSAC_USERNAME=... IGNORE_HTTPS_ERRORS=true | ||
| read -rs OSAC_PASSWORD && export OSAC_PASSWORD | ||
| pnpm playwright | ||
| ``` | ||
|
|
||
| ## 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`) 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 `smoke` and `chromium` projects) declares a dependency | ||
| on `setup` and loads that saved session before the test body runs — so | ||
| tests start already logged in. | ||
|
|
||
| `pnpm playwright` (and `pnpm playwright:setup`) always re-run `setup` first, | ||
| regenerating `.auth/user.json` fresh. `pnpm playwright:run` skips `setup` | ||
| entirely and reuses whatever session is already on disk — see | ||
| [Running](#running). | ||
|
|
||
| `.auth/user.json` contains a live, real session (the actual `osac-access` | ||
| cookie) — it's gitignored and must never be committed or shared, and | ||
| `auth.setup.ts` restricts it to `0600` (and its directory to `0700`) so other | ||
| local accounts on a shared machine can't read it. | ||
|
|
||
| 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. | ||
|
|
||
| ## Smoke Test | ||
|
|
||
| `src/smoke.spec.ts` is the one persisted, committed spec in this package — a | ||
| harness self-check, not a feature test. It logs in (via `setup`) and asserts | ||
| that the app actually loads and renders its masthead. Its job is to answer | ||
| "is the harness broken, or is my change broken?" before you spend time | ||
| writing a real `scratch/` spec: if the smoke test fails, the problem is | ||
| almost certainly auth, config, or environment — not the change you're trying | ||
| to verify. | ||
|
|
||
| It runs automatically as part of `pnpm playwright` (and standalone via `pnpm | ||
| --filter @osac/playwright exec playwright test --project=smoke`). Unlike | ||
| `scratch/` specs, do not delete or repurpose it, and do not add more specs | ||
| here — this package still isn't a test suite; one fixed sanity check is the | ||
| exception, not a precedent. | ||
|
|
||
| ## Writing a test | ||
|
|
||
| **This is not a test suite.** Beyond the one smoke test above, 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/` (that's | ||
| reserved for the harness and the smoke test), 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). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "name": "@osac/playwright", | ||
| "version": "0.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "playwright": "playwright test", | ||
| "playwright:setup": "playwright test --project=setup", | ||
| "playwright:run": "playwright test --project=chromium --no-deps", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "^1.62.0", | ||
| "@types/node": "^20.19.41", | ||
| "typescript": "~5.9.3" | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| 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: 'smoke', | ||
| testDir: './src', | ||
| testMatch: /smoke\.spec\.ts/, | ||
| use: { ...devices['Desktop Chrome'], storageState: AUTH_FILE }, | ||
| dependencies: ['setup'], | ||
| }, | ||
| { | ||
| name: 'chromium', | ||
| testDir: scratchDir, | ||
| use: { ...devices['Desktop Chrome'], storageState: AUTH_FILE }, | ||
| dependencies: ['setup'], | ||
| }, | ||
| ], | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const AUTH_FILE = '.auth/user.json'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { expect, test as setup } from '@playwright/test'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
|
|
||
| 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(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible(); | ||
|
|
||
| await page.context().storageState({ path: AUTH_FILE }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| // AUTH_FILE holds a live, real Keycloak session cookie — restrict it to the | ||
| // current user so other local accounts on a shared machine can't reuse it. | ||
| fs.chmodSync(path.dirname(AUTH_FILE), 0o700); | ||
| fs.chmodSync(AUTH_FILE, 0o600); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| test('authenticated app loads and shows the masthead', async ({ page }) => { | ||
| await page.goto('/'); | ||
|
|
||
| await expect(page.getByRole('banner')).toBeVisible(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "include": ["src", "playwright.config.ts"] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.