|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to AI coding tools when working with the `@red-hat-developer-hub/e2e-test-utils` package. |
| 4 | + |
| 5 | +## Package Purpose |
| 6 | + |
| 7 | +A shared npm package (`@red-hat-developer-hub/e2e-test-utils`) that provides everything needed to write and run E2E tests for Red Hat Developer Hub (RHDH) plugins. It handles: |
| 8 | + |
| 9 | +- Deploying RHDH to OpenShift (Helm or Operator) |
| 10 | +- Deploying Keycloak for authentication |
| 11 | +- Playwright test fixtures, helpers, and page objects |
| 12 | +- Dynamic plugin configuration with metadata-based OCI resolution |
| 13 | +- Config file merging (common defaults + auth + user overrides) |
| 14 | +- Kubernetes API operations |
| 15 | +- Per-project namespace teardown in CI |
| 16 | + |
| 17 | +Published to npm under `@red-hat-developer-hub` scope. Primary consumers are overlay workspaces in `rhdh-plugin-export-overlays`. |
| 18 | + |
| 19 | +## Project Structure |
| 20 | + |
| 21 | +``` |
| 22 | +src/ |
| 23 | +├── deployment/ |
| 24 | +│ ├── rhdh/ # RHDHDeployment class — core deployment orchestration |
| 25 | +│ │ ├── deployment.ts # Main class (configure, deploy, waitUntilReady, teardown) |
| 26 | +│ │ ├── types.ts # DeploymentOptions, DeploymentConfig |
| 27 | +│ │ ├── constants.ts # Config paths, auth providers, chart URLs |
| 28 | +│ │ └── config/ # YAML templates (common/, auth/, helm/, operator/) |
| 29 | +│ ├── keycloak/ # KeycloakHelper — Keycloak Helm deployment + OIDC setup |
| 30 | +│ └── orchestrator/ # Workflow orchestrator installer |
| 31 | +├── playwright/ |
| 32 | +│ ├── fixtures/test.ts # Custom fixtures: rhdh, uiHelper, loginHelper, baseURL |
| 33 | +│ ├── base-config.ts # Base Playwright config (reporters, timeouts, video/trace) |
| 34 | +│ ├── global-setup.ts # Pre-test: check binaries, detect cluster, deploy Keycloak |
| 35 | +│ ├── run-once.ts # Cross-worker one-time execution with file locking |
| 36 | +│ ├── teardown-reporter.ts # Per-project namespace cleanup (CI only) |
| 37 | +│ ├── teardown-namespaces.ts # Custom namespace registry for teardown |
| 38 | +│ ├── helpers/ # LoginHelper, UIhelper, APIHelper, RbacApiHelper, etc. |
| 39 | +│ ├── pages/ # Page objects: CatalogPage, HomePage, ExtensionsPage, etc. |
| 40 | +│ └── page-objects/ # Shared element selectors/constants |
| 41 | +├── utils/ |
| 42 | +│ ├── plugin-metadata.ts # Plugin discovery, OCI resolution, PR/nightly modes |
| 43 | +│ ├── workspace-paths.ts # Path resolution from Playwright testDir (not CWD) |
| 44 | +│ ├── kubernetes-client.ts # KubernetesClientHelper (K8s API wrapper) |
| 45 | +│ ├── merge-yamls.ts # Deep merge with array strategies (replace, concat, byKey) |
| 46 | +│ ├── bash.ts # zx shell wrapper ($, runQuietUnlessFailure) |
| 47 | +│ └── common.ts # envsubst, requireEnv helpers |
| 48 | +└── eslint/ |
| 49 | + └── base.config.ts # createEslintConfig() factory for consumers |
| 50 | +``` |
| 51 | + |
| 52 | +## Package Exports |
| 53 | + |
| 54 | +| Import Path | What It Provides | |
| 55 | +| --------------------- | ------------------------------------------------------------------------------ | |
| 56 | +| `./test` | `test`, `expect` — Playwright fixtures with RHDH-specific fixtures | |
| 57 | +| `./playwright-config` | `defineConfig`, `baseConfig` — base Playwright configuration | |
| 58 | +| `./rhdh` | `RHDHDeployment` — deployment orchestration class | |
| 59 | +| `./utils` | `$`, `KubernetesClientHelper`, `WorkspacePaths`, `envsubst`, `mergeYamlFiles` | |
| 60 | +| `./helpers` | `UIhelper`, `LoginHelper`, `APIHelper`, `RbacApiHelper`, `AccessibilityHelper` | |
| 61 | +| `./pages` | `CatalogPage`, `HomePage`, `ExtensionsPage`, etc. | |
| 62 | +| `./keycloak` | `KeycloakHelper` — Keycloak deployment and OIDC configuration | |
| 63 | +| `./teardown` | `registerTeardownNamespace` — custom namespace cleanup registration | |
| 64 | +| `./orchestrator` | `installOrchestrator` — workflow orchestrator deployment | |
| 65 | +| `./eslint` | `createEslintConfig` — ESLint config factory | |
| 66 | +| `./tsconfig` | Base TypeScript configuration (JSON, not code) | |
| 67 | + |
| 68 | +## Build System |
| 69 | + |
| 70 | +```bash |
| 71 | +yarn build # Clean + tsc + copy config YAML files + copy shell scripts to dist/ |
| 72 | +yarn test # node --test "dist/**/*.test.js" (must build first) |
| 73 | +yarn check # typecheck + lint + prettier |
| 74 | +``` |
| 75 | + |
| 76 | +- **TypeScript**: ES2022 target, ESNext modules, strict mode |
| 77 | +- **Module system**: ESM (`"type": "module"`) |
| 78 | +- **Build output**: `dist/` (JS + .d.ts declarations + config YAMLs) |
| 79 | +- **Config files**: Copied as-is from `src/deployment/*/config/` to `dist/` during build |
| 80 | +- **Package manager**: Yarn v3.8.7, Node >= 22.18.0 |
| 81 | + |
| 82 | +## Key Architectural Concepts |
| 83 | + |
| 84 | +### Configuration Merging (3-level cascade) |
| 85 | + |
| 86 | +``` |
| 87 | +Package defaults (config/common/) |
| 88 | + ↓ deep merge |
| 89 | +Auth-specific (config/auth/{keycloak|guest|github}/) |
| 90 | + ↓ deep merge |
| 91 | +User config (workspace's tests/config/*.yaml) |
| 92 | + ↓ |
| 93 | += Final merged config |
| 94 | +``` |
| 95 | + |
| 96 | +Array merge uses "replace" strategy by default. Plugin arrays use `byKey: "package"` with normalized keys (strips trailing `-dynamic`). |
| 97 | + |
| 98 | +### Plugin Metadata Resolution |
| 99 | + |
| 100 | +`RHDHDeployment.deploy()` internally calls `processPluginsForDeployment()` which operates in two modes: |
| 101 | + |
| 102 | +| Mode | Detection | Behavior | |
| 103 | +| ------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- | |
| 104 | +| **PR check** | `GIT_PR_NUMBER` is set | Injects metadata configs + resolves to PR-built OCI URLs (`pr_{number}__{version}`) | |
| 105 | +| **Nightly** | `E2E_NIGHTLY_MODE=true` or `JOB_NAME` contains `periodic-` | Resolves to released OCI refs from `spec.dynamicArtifact` in metadata; skips config injection | |
| 106 | +| **Local dev** | Neither set | Uses local paths as-is; injects metadata configs | |
| 107 | + |
| 108 | +Priority: `GIT_PR_NUMBER` (forces PR mode) > `E2E_NIGHTLY_MODE` > `JOB_NAME` |
| 109 | + |
| 110 | +When no `dynamic-plugins.yaml` exists in the workspace, plugins are auto-generated from `metadata/*.yaml` files. |
| 111 | + |
| 112 | +### WorkspacePaths |
| 113 | + |
| 114 | +Static utility that resolves config file paths from `test.info().project.testDir` (Playwright-provided absolute path) instead of `process.cwd()`. This is critical because tests can run from two contexts: |
| 115 | + |
| 116 | +- Workspace level: `cd workspaces/tech-radar/e2e-tests && yarn test` |
| 117 | +- Repo root: `./run-e2e.sh -w tech-radar` |
| 118 | + |
| 119 | +The worker fixture also does `process.chdir(e2eRoot)` as a complementary safety net. |
| 120 | + |
| 121 | +### Playwright Fixtures |
| 122 | + |
| 123 | +Worker fixture creates `RHDHDeployment(projectName)` from the Playwright project name (which becomes the K8s namespace) and sets CWD to the workspace e2e-tests directory. Test-scoped fixtures (`uiHelper`, `loginHelper`, `baseURL`) are created fresh per test. See the [consumer code example](#typical-test-structure) for usage. |
| 124 | + |
| 125 | +### runOnce — Cross-Worker Deduplication |
| 126 | + |
| 127 | +`deploy()` uses `runOnce()` internally to execute exactly once per test run, even when Playwright restarts workers after test failures. Uses file-based flags with `proper-lockfile` in `/tmp/playwright-once-{ppid}/`. |
| 128 | + |
| 129 | +### Teardown Reporter |
| 130 | + |
| 131 | +Custom Playwright reporter that deletes namespaces per-project (not per-suite) as tests complete. Only active when `CI=true`. Needed because: |
| 132 | + |
| 133 | +- `afterAll` hooks fire on worker restart (before retries can run) |
| 134 | +- Worker fixture teardown has the same problem |
| 135 | +- `globalTeardown` has no visibility into which projects ran |
| 136 | + |
| 137 | +### Global Setup |
| 138 | + |
| 139 | +Runs once before all tests: |
| 140 | + |
| 141 | +1. Checks required binaries (`oc`, `kubectl`, `helm`) |
| 142 | +2. Auto-detects cluster domain (`K8S_CLUSTER_ROUTER_BASE`) |
| 143 | +3. Deploys Keycloak (unless `SKIP_KEYCLOAK_DEPLOYMENT=true`) |
| 144 | + |
| 145 | +### RHDHDeployment.deploy() Flow |
| 146 | + |
| 147 | +1. Merges config files (common + auth + user) |
| 148 | +2. Injects plugin metadata into dynamic plugins config (PR/local mode) |
| 149 | +3. Applies ConfigMaps (app-config, dynamic-plugins) |
| 150 | +4. Applies Secrets (with `envsubst` for environment variable substitution) |
| 151 | +5. Installs RHDH via Helm or Operator |
| 152 | +6. Waits for readiness: pod `Ready=True` + route HTTP health check |
| 153 | +7. Sets `RHDH_BASE_URL` environment variable |
| 154 | + |
| 155 | +Helm upgrades perform `scaleDownAndRestart()` to avoid `MigrationLocked` errors. Fresh installs skip this. |
| 156 | + |
| 157 | +## Key Environment Variables |
| 158 | + |
| 159 | +| Variable | Purpose | Default | |
| 160 | +| ------------------------------------- | ------------------------------------------------ | -------- | |
| 161 | +| `RHDH_VERSION` | RHDH version to deploy | `"next"` | |
| 162 | +| `INSTALLATION_METHOD` | `"helm"` or `"operator"` | `"helm"` | |
| 163 | +| `GIT_PR_NUMBER` | PR number — triggers PR-mode OCI resolution | - | |
| 164 | +| `E2E_NIGHTLY_MODE` | `"true"` or `"1"` — nightly mode | - | |
| 165 | +| `JOB_NAME` | CI job name; `periodic-` prefix triggers nightly | - | |
| 166 | +| `SKIP_KEYCLOAK_DEPLOYMENT` | Skip Keycloak in global setup | - | |
| 167 | +| `K8S_CLUSTER_ROUTER_BASE` | Cluster domain (auto-detected) | - | |
| 168 | +| `CI` | Enables `forbidOnly`, teardown reporter | - | |
| 169 | +| `RHDH_SKIP_PLUGIN_METADATA_INJECTION` | Skip metadata config injection in PR mode | - | |
| 170 | +| `CATALOG_INDEX_IMAGE` | Override catalog index image | - | |
| 171 | + |
| 172 | +## Consumer Perspective (Overlay Workspaces) |
| 173 | + |
| 174 | +Changes to this package affect ~67 overlay workspaces. Understanding how they use the package is critical before modifying APIs. |
| 175 | + |
| 176 | +### Typical Test Structure |
| 177 | + |
| 178 | +```typescript |
| 179 | +import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test"; |
| 180 | +import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils"; |
| 181 | + |
| 182 | +test.describe("My Plugin", () => { |
| 183 | + test.beforeAll(async ({ rhdh }) => { |
| 184 | + // 1. Pre-deployment setup (external services, K8s resources) |
| 185 | + await $`bash ${setupScript} ${rhdh.deploymentConfig.namespace}`; |
| 186 | + process.env.SERVICE_URL = await rhdh.k8sClient.getRouteLocation( |
| 187 | + namespace, |
| 188 | + "my-svc", |
| 189 | + ); |
| 190 | + |
| 191 | + // 2. Configure + deploy RHDH |
| 192 | + await rhdh.configure({ auth: "keycloak" }); |
| 193 | + await rhdh.deploy(); |
| 194 | + }); |
| 195 | + |
| 196 | + test.beforeEach(async ({ loginHelper }) => { |
| 197 | + await loginHelper.loginAsKeycloakUser(); |
| 198 | + }); |
| 199 | + |
| 200 | + test("verify feature", async ({ uiHelper }) => { |
| 201 | + await uiHelper.openSidebar("My Plugin"); |
| 202 | + await uiHelper.verifyHeading("Expected Title"); |
| 203 | + }); |
| 204 | +}); |
| 205 | +``` |
| 206 | + |
| 207 | +### Consumer Gotchas |
| 208 | + |
| 209 | +- **Config paths are workspace-relative** — `appConfig: "tests/config/app-config.yaml"` resolves from the workspace's `e2e-tests/` directory via `WorkspacePaths`, not from `process.cwd()`. |
| 210 | +- **`$` shell is for setup, not assertions** — used in `beforeAll` for deployment scripts. Use `$({ stdio: "pipe" })` to capture output. |
| 211 | +- **`rhdh.configure()` must precede `rhdh.deploy()`** — every workspace follows this order. |
| 212 | +- **`defineConfig` only needs `projects`** — base config handles reporters, timeouts, video/trace, global setup. Consumers just specify project names (which become K8s namespaces). |
| 213 | + |
| 214 | +### What Consumers Use vs What's Internal |
| 215 | + |
| 216 | +| Consumer-facing (public API) | Internal (used by deploy()) | |
| 217 | +| ----------------------------------------------- | ------------------------------- | |
| 218 | +| `test`, `expect` fixtures | `processPluginsForDeployment()` | |
| 219 | +| `rhdh.configure()`, `rhdh.deploy()` | `isNightlyJob()` | |
| 220 | +| `UIhelper`, `LoginHelper`, `APIHelper` | `generatePluginsFromMetadata()` | |
| 221 | +| `$`, `WorkspacePaths`, `KubernetesClientHelper` | `injectMetadataConfig()` | |
| 222 | +| `defineConfig` from `./playwright-config` | `resolvePluginPackages()` | |
| 223 | +| `registerTeardownNamespace` from `./teardown` | `disablePluginWrappers()` | |
| 224 | + |
| 225 | +## Testing |
| 226 | + |
| 227 | +Tests use Node.js built-in `node:test` module (not Playwright): |
| 228 | + |
| 229 | +```bash |
| 230 | +yarn build && yarn test |
| 231 | +``` |
| 232 | + |
| 233 | +Test files: |
| 234 | + |
| 235 | +- `src/deployment/rhdh/deployment.test.ts` — plugin merge behavior |
| 236 | +- `src/utils/merge-yamls.test.ts` — YAML merge strategies |
| 237 | +- `src/utils/tests/plugin-metadata.*.test.ts` — metadata resolution (PR, nightly, fixtures) |
| 238 | + |
| 239 | +## Naming Conventions |
| 240 | + |
| 241 | +- **Class**: `UIhelper` (capital U, lowercase h) — matches source code |
| 242 | +- **Fixture**: `uiHelper` (camelCase) — used in test fixtures |
| 243 | +- **Method names**: Follow source exactly (e.g., `verifyTextinCard` not `verifyTextInCard`) |
| 244 | + |
| 245 | +## Documentation |
| 246 | + |
| 247 | +VitePress docs in `docs/` — standalone package with its own `package.json`. See `docs/CLAUDE.md` for documentation-specific guidance. |
| 248 | + |
| 249 | +```bash |
| 250 | +cd docs && yarn install && yarn dev # http://localhost:5173 |
| 251 | +``` |
| 252 | + |
| 253 | +Deployed to GitHub Pages via `.github/workflows/deploy-docs.yml` on pushes to `main`. |
| 254 | + |
| 255 | +## Common Tasks |
| 256 | + |
| 257 | +### Adding a New Export Path |
| 258 | + |
| 259 | +1. Add the entry to `package.json` `exports` with `types` and `default` |
| 260 | +2. Create the source file and its `index.ts` barrel export |
| 261 | +3. Document in the VitePress docs (guide + API sections) |
| 262 | + |
| 263 | +### Adding a New Auth Provider |
| 264 | + |
| 265 | +1. Create config files in `src/deployment/rhdh/config/auth/<provider>/` |
| 266 | +2. Add the provider to the `AUTH_PROVIDERS` constant in `constants.ts` |
| 267 | +3. Update the `auth` type in `types.ts` |
| 268 | + |
| 269 | +### Modifying Config Merging |
| 270 | + |
| 271 | +Config merge order is defined in `deployment.ts` `_buildDeploymentConfig()`. Array merge strategies are in `merge-yamls.ts`. Plugin-specific merging uses `getNormalizedPluginMergeKey()` to deduplicate entries with/without `-dynamic` suffix. |
| 272 | + |
| 273 | +### Updating Plugin Metadata Logic |
| 274 | + |
| 275 | +All in `src/utils/plugin-metadata.ts`. Key functions: |
| 276 | + |
| 277 | +- `isNightlyJob()` — mode detection |
| 278 | +- `processPluginsForDeployment()` — unified entry point for PR + nightly |
| 279 | +- `generatePluginsFromMetadata()` — auto-generate from metadata files |
| 280 | +- `resolvePluginPackages()` — OCI URL resolution |
| 281 | +- `injectMetadataConfig()` — merge metadata configs into plugin entries |
| 282 | +- `disablePluginWrappers()` — disable local wrappers when using OCI images |
| 283 | + |
| 284 | +Test thoroughly — changes affect all overlay workspaces. Run `yarn build && yarn test` to verify. |
0 commit comments