diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml new file mode 100644 index 0000000000..d6f5ef00b9 --- /dev/null +++ b/.github/workflows/e2e-cluster-free.yaml @@ -0,0 +1,92 @@ +name: E2E Cluster-free + +# Runs the cluster-free local E2E harness (RHIDP-13501 / RHIDP-15075): boots the +# backend and the legacy app dev server in-process and drives Playwright against +# them, with dynamic plugins installed from the public OCI registry (ghcr) via +# the install-dynamic-plugins CLI (skopeo). No OpenShift/Kubernetes cluster or +# image. See docs/e2e-tests/local-e2e-harness.md. + +on: + pull_request: + paths: + - "e2e-tests/**" + - "app-config*.yaml" + - ".github/workflows/e2e-cluster-free.yaml" + push: + branches: + - "main" + - "release-*" + paths: + - "e2e-tests/**" + - "app-config*.yaml" + - ".github/workflows/e2e-cluster-free.yaml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + e2e: + name: e2e + runs-on: ubuntu-latest + # Playwright image: browsers + OS deps preinstalled, so no browser-install step + # (which hung on plain ubuntu runners). Keep the tag in sync with the + # @playwright/test version in e2e-tests/package.json (currently 1.61.1) — a + # mismatch fails with "Executable doesn't exist at /ms-playwright/...". + container: + image: mcr.microsoft.com/playwright:v1.61.1-noble + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: ".nvmrc" + + - name: Enable Corepack (vendored yarn 4) + run: corepack enable + + - name: Cache yarn global cache + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: ~/.yarn/berry/cache + key: yarn-${{ hashFiles('yarn.lock', 'e2e-tests/yarn.lock') }} + restore-keys: | + yarn- + + - name: Install skopeo + run: | + apt-get update + apt-get install -y --no-install-recommends skopeo + + - name: Install dependencies (root) + run: yarn install + + - name: Install dependencies (e2e-tests) + working-directory: ./e2e-tests + run: yarn install --mode=skip-build + + - name: Populate dynamic-plugins-root (OCI, no source build) + # install-dynamic-plugins pulls the harness plugin set from the public OCI + # registry (ghcr) via skopeo — the core plugins in the catalog index's + # default.yaml reference local ./dynamic-plugins/dist paths that only exist + # after a source build, so we install the OCI-published builds instead. + run: ./e2e-tests/local-harness/populate.sh + + - name: Run cluster-free E2E (legacy app) + working-directory: ./e2e-tests + run: yarn e2e:legacy-local + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-report-legacy-local + path: e2e-tests/playwright-report-legacy-local + retention-days: 7 diff --git a/.gitignore b/.gitignore index 6bf450e826..8345b9d48f 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ site # Dynamic plugins root content dynamic-plugins-root/* !dynamic-plugins-root/.gitkeep +# install-dynamic-plugins config copied to the repo root by e2e-tests/local-harness/populate.sh +/dynamic-plugins.yaml #dev caches .webpack-cache diff --git a/app-config.local-e2e.yaml b/app-config.local-e2e.yaml new file mode 100644 index 0000000000..bca1b8657b --- /dev/null +++ b/app-config.local-e2e.yaml @@ -0,0 +1,42 @@ +# Config overlay for the cluster-free local E2E harness (legacy `packages/app`, Tier B). +# +# Layered on top of app-config.yaml and app-config.dynamic-plugins.yaml to run +# Playwright E2E without an OpenShift/Kubernetes cluster or container images: +# +# yarn --cwd e2e-tests e2e:legacy-local +# +# It enables guest sign-in (the auth backend rejects guest unless a provider is +# configured) and pins the in-memory SQLite database so a single `run` is fully +# self-contained. See docs/e2e-tests/local-e2e-harness.md. +# +# NOTE: test-only. This grants unauthenticated guest access +# (dangerouslyAllowOutsideDevelopment) and must never be layered into a +# production config. +auth: + environment: development + providers: + guest: + userEntityRef: user:default/guest + # Required because auth.environment may resolve outside "development" in CI. + dangerouslyAllowOutsideDevelopment: true + +backend: + database: + client: better-sqlite3 + connection: ":memory:" + +# The e2e specs are written against the CI deployment's menu customization +# (.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml): APIs and +# Learning Paths nest under a "References" sidebar group. Mirror it here so +# navigation helpers like openSidebarButton("References") work off-cluster. +dynamicPlugins: + frontend: + default.main-menu-items: + menuItems: + default.list: + title: References + icon: bookmarks + default.apis: + parent: default.list + default.learning-path: + parent: default.list diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md new file mode 100644 index 0000000000..f6bf586cfa --- /dev/null +++ b/docs/e2e-tests/local-e2e-harness.md @@ -0,0 +1,138 @@ +# Cluster-free local E2E harness + +Spike deliverable for **RHIDP-13501 — E2E Test Optimization (Layer 4a)**, building on +the PoC in [PR #4523](https://github.com/redhat-developer/rhdh/pull/4523) and the +backend dynamic-plugin loader from RHIDP-13508. + +## Goal + +Run real Playwright E2E against RHDH **without** an OpenShift/Kubernetes cluster or +container images — a single `run` that boots the backend and the legacy frontend dev +server in-process and drives a browser against them. + +The harness targets the legacy frontend (`packages/app`, Tier B): it is what RHDH ships +today, and **the existing Playwright specs already target it**, so they run unmodified. +Dynamic frontend plugins load through Scalprum exactly as in-cluster (the legacy +`scalprum-backend` serves the plugin config by default). + +The guest-auth + in-memory-SQLite overlay `app-config.local-e2e.yaml` is layered on top +of `app-config.yaml`. Guest sign-in must be configured explicitly — the auth backend +otherwise rejects guest with _"you must … configure the auth backend to support guest +sign in."_ + +### 1. Populate `dynamic-plugins-root` (one-time) + +Run the same script CI uses — it installs the harness plugin set +(`e2e-tests/local-harness/dynamic-plugins.yaml`) from the public OCI registry (ghcr) +via `install-dynamic-plugins` + skopeo, pinned to the same CLI version as CI. No +source build needed; works from a fresh clone. Requires skopeo (preinstalled in CI; +`brew install skopeo` on macOS): + +```bash +./e2e-tests/local-harness/populate.sh +``` + +Alternatives: + +- **Catalog index** — the index's `dynamic-plugins.default.yaml` references the core + plugins by local `./dynamic-plugins/dist/…` paths that only exist after a source + build, so on a fresh clone most plugins are skipped. Use only after building + `dynamic-plugins` from source (main -> `:latest`; release branches -> the matching + `:1.y` tag): + + ```bash + CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:latest \ + npx @red-hat-developer-hub/cli-module-install-dynamic-plugins install dynamic-plugins-root + ``` + +- **Offline from-source** (frontend plugins only; requires a reconciled workspace — + see "Known issues"): + + ```bash + yarn --cwd dynamic-plugins export-dynamic + yarn --cwd dynamic-plugins copy-dynamic-plugins ../dynamic-plugins-root + ``` + +### 2. Run + +```bash +yarn --cwd e2e-tests e2e:legacy-local +``` + +Playwright (`playwright.legacy-local.config.ts`) boots the backend and the legacy app +dev server with `app-config.yaml` + `app-config.dynamic-plugins.yaml` + +`app-config.local-e2e.yaml`. A `globalSetup` first fails fast with the populate command +if `dynamic-plugins-root` has no plugins. + +The run is scoped to tests tagged `@cluster-free` within the spec files allowlisted in +`testMatch`. To widen coverage, tag a validated test with `@cluster-free` and add its +spec file to `testMatch`; if the test needs extra plugins, add them (with their +`pluginConfig`) to `e2e-tests/local-harness/dynamic-plugins.yaml` and re-run +`populate.sh` (see "Known issues"). + +### Verified + +With plugins populated, the legacy app renders the full production RHDH UI off-cluster +(branding, sidebar, global header, and Quick Access from the dynamic plugins). The +existing specs **pass unmodified**: + +- `guest-signin-happy-path` — all three tests: home page (dynamic-home-page plugin), + Settings and Sign-out (navigation via the global-header profile dropdown, using the + plugin's canonical `pluginConfig` merged through the generated + `dynamic-plugins-root/app-config.dynamic-plugins.yaml`, exactly as in-cluster). +- `learning-path-page` — renders from the static fallback data bundled with + `packages/app`; the "References" sidebar group mirrors the CI menu customization via + `app-config.local-e2e.yaml`. + +## CI + +`.github/workflows/e2e-cluster-free.yaml` runs this harness on GitHub Actions in a +cluster-free phase: it installs deps + skopeo, populates `dynamic-plugins-root` via +`./e2e-tests/local-harness/populate.sh` (the harness plugin set from the public OCI +registry, ghcr), then runs `yarn e2e:legacy-local`. No cluster or container image is +built. It triggers on `e2e-tests/**` and `app-config*.yaml` changes; the scope can +widen to `packages/app/**` / `packages/backend/**` once it is proven stable. + +## Why the legacy app, not app-next + +The harness targets the legacy app because **dynamic frontend plugins do not load on +`packages/app-next` yet**: app-next's `dynamicFrontendFeaturesLoader()` fetches Module +Federation remotes from the backend, but that endpoint is no-op'd unless +`ENABLE_STANDARD_MODULE_FEDERATION=true`, and even then RHDH's exported dynamic frontend +plugins do not contain standard MF assets (see `packages/backend/src/index.ts`). Until +that lands upstream, app-next can only exercise core/static plugin UIs. An app-next +harness is tracked as a follow-up (RHIDP-13501 / spike RHIDP-15075). + +## vs. rhdh-local + +[`rhdh-local`](https://github.com/redhat-developer/rhdh-local) runs RHDH via +Podman/Docker Compose using the **production container image**. It is great for manual +feature testing with guest auth and UI-installed plugins, but it is **container-based**: +it requires a container runtime and pulling/running the RHDH image. For fast automated +E2E it is heavier than this in-process harness (no image pull, no container runtime — +just `run`), which is why this harness boots the dev servers directly instead. + +## Known issues / limits + +- **Workspace must be reconciled for the offline (from-source) populate path.** If + `node_modules` is out of sync with `yarn.lock` (e.g. just after a rebase that changed + dependency versions), backend dynamic-plugin builds fail with version-mismatch errors + and yarn may not surface workspace bins. Run `yarn install` first. The + `install-dynamic-plugins` populate path avoids building from source and is unaffected. +- **Re-run `populate.sh` after changing the harness plugin set.** The `pluginConfig` + blocks in `e2e-tests/local-harness/dynamic-plugins.yaml` (e.g. the global-header + mount points) only take effect through the generated + `dynamic-plugins-root/app-config.dynamic-plugins.yaml`, which the webServer loads + last. A stale populate leaves plugins loaded but unconfigured (the header renders + empty). +- **Specs that need CI test data are not enabled yet.** `settings.spec.ts` asserts + ownership entities ("Guest User, team-a") that come from catalog locations in the CI + config map; `home-page-customization.spec.ts` needs the home-page card customization + from `.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml`. Enabling them + means mirroring that data/config into the harness overlay. +- **Live-external-service specs** (real k8s cluster, GitHub org, Quay, Tekton, Keycloak) + still need those services or mocks; this harness covers UI/plugin-rendering scenarios + that don't require live external infra. +- **`janus-cli` / `backstage-cli`** live in the repo-root `node_modules/.bin`, which yarn + does not surface for the `app`/`backend` workspaces, so the webServer commands invoke + them directly with the root `.bin` prepended to `PATH`. diff --git a/e2e-tests/local-harness/dynamic-plugins.yaml b/e2e-tests/local-harness/dynamic-plugins.yaml new file mode 100644 index 0000000000..2db1672dfc --- /dev/null +++ b/e2e-tests/local-harness/dynamic-plugins.yaml @@ -0,0 +1,132 @@ +# Cluster-free harness plugin set — installed by ./populate.sh via +# install-dynamic-plugins from OCI (ghcr, pulled with skopeo), so no +# dynamic-plugins/dist source build is needed. +# +# app-config.dynamic-plugins.yaml (loaded by the harness) already configures these +# plugins, so the existing RHDH specs render off-cluster. +# +# NOTE: OCI tags are backstage 1.49.4 (RHDH is on 1.52.0) — bump to matching tags when +# the overlays repo publishes them. +plugins: + # Home page: route / -> DynamicHomePage, SearchBar, QuickAccessCard, Starred Entities. + - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-dynamic-home-page:bs_1.49.4__1.13.1!red-hat-developer-hub-backstage-plugin-dynamic-home-page + disabled: false + # Global header: top bar + profile dropdown (Settings / Sign-out navigation). + # + # The repo's static app-config.dynamic-plugins.yaml only mounts the bare GlobalHeader + # container (no children), so the header renders empty off-cluster. In-cluster the full + # config comes from the plugin's pluginConfig in the catalog index's + # dynamic-plugins.default.yaml; the block below is that canonical config, copied from + # rhdh-plugins workspaces/global-header/plugins/global-header/app-config.dynamic.yaml. + # install-dynamic-plugins merges it into the generated + # dynamic-plugins-root/app-config.dynamic-plugins.yaml, which the harness loads last — + # exactly how the production container consumes it. + - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-global-header:bs_1.49.4__1.21.6!red-hat-developer-hub-backstage-plugin-global-header + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-global-header: + translationResources: + - importName: globalHeaderTranslations + ref: globalHeaderTranslationRef + mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-sidebar # above-main-content | above-sidebar + - mountPoint: global.header/component + importName: CompanyLogo + config: + priority: 200 + props: + to: "/" + - mountPoint: global.header/component + importName: SearchComponent + config: + priority: 100 + - mountPoint: global.header/component + importName: Spacer + config: + priority: 99 + props: + growFactor: 0 + - mountPoint: global.header/component + importName: HeaderIconButton + config: + priority: 90 + props: + title: Self-service + titleKey: create.title + icon: add + to: create + - mountPoint: global.header/component + importName: StarredDropdown + config: + priority: 85 + - mountPoint: global.header/component + importName: ApplicationLauncherDropdown + config: + priority: 82 + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Documentation + priority: 150 + props: + title: Developer Hub + titleKey: applicationLauncher.developerHub + icon: developerHub + link: https://docs.redhat.com/en/documentation/red_hat_developer_hub + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Developer Tools + priority: 100 + props: + title: RHDH Local + titleKey: applicationLauncher.rhdhLocal + icon: developerHub + link: https://github.com/redhat-developer/rhdh-local + - mountPoint: global.header/component + importName: HelpDropdown + config: + priority: 80 + - mountPoint: global.header/help + importName: SupportButton + config: + priority: 10 + - mountPoint: global.header/component + importName: NotificationButton + config: + priority: 70 + - mountPoint: global.header/component + importName: Divider + config: + priority: 50 + - mountPoint: global.header/component + importName: ProfileDropdown + config: + priority: 10 + - mountPoint: global.header/profile + importName: MenuItemLink + config: + priority: 100 + props: + title: Settings + titleKey: profile.settings + link: /settings + icon: manageAccounts + - mountPoint: global.header/profile + importName: MenuItemLink + config: + priority: 90 + props: + title: My profile + titleKey: profile.myProfile + type: myProfile + icon: account + - mountPoint: global.header/profile + importName: LogoutButton + config: + priority: 10 diff --git a/e2e-tests/local-harness/populate.sh b/e2e-tests/local-harness/populate.sh new file mode 100755 index 0000000000..2bb837a555 --- /dev/null +++ b/e2e-tests/local-harness/populate.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# Populates dynamic-plugins-root for the cluster-free E2E harness — the single +# source of truth for the populate step (CI, the docs, and the global-setup +# error message all point here). +# +# Installs the plugin set from e2e-tests/local-harness/dynamic-plugins.yaml +# from the public OCI registry (ghcr) via install-dynamic-plugins + skopeo — +# no dynamic-plugins/dist source build and no cluster. Requires skopeo +# (preinstalled in CI; `brew install skopeo` on macOS). +set -e + +# Pinned so local runs install the exact CLI version CI uses. +CLI_VERSION="0.2.0" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +mkdir -p dynamic-plugins-root +# The CLI hardcodes ./dynamic-plugins.yaml (cwd) as its config file; the copy at +# the repo root is gitignored. +cp e2e-tests/local-harness/dynamic-plugins.yaml dynamic-plugins.yaml +npx -y "@red-hat-developer-hub/cli-module-install-dynamic-plugins@$CLI_VERSION" install dynamic-plugins-root diff --git a/e2e-tests/package.json b/e2e-tests/package.json index f12602e0d6..c6b6ee9322 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -25,7 +25,8 @@ "fmt": "oxfmt .", "fmt:check": "oxfmt --check .", "postinstall": "playwright install chromium", - "shellcheck": "git ls-files -z '*.sh' | xargs -0 shellcheck --severity=warning --color=always" + "shellcheck": "git ls-files -z '*.sh' | xargs -0 shellcheck --severity=warning --color=always", + "e2e:legacy-local": "playwright test --config=playwright.legacy-local.config.ts" }, "dependencies": { "@azure/arm-network": "34.2.0", diff --git a/e2e-tests/playwright.legacy-local.config.ts b/e2e-tests/playwright.legacy-local.config.ts new file mode 100644 index 0000000000..598ae3e189 --- /dev/null +++ b/e2e-tests/playwright.legacy-local.config.ts @@ -0,0 +1,114 @@ +import { resolve } from "path"; + +import { defineConfig, devices } from "@playwright/test"; + +/** + * Cluster-free local E2E harness for the legacy frontend (`packages/app`) — Tier B. + * + * RHIDP-13501 (E2E Test Optimization). Runs the EXISTING Playwright specs against a + * production-faithful RHDH instance with dynamic plugins loaded, without an + * OpenShift/Kubernetes cluster or container images. Playwright boots the backend and + * the legacy app dev server itself and drives the browser against them. + * + * # one-time: populate dynamic-plugins-root (same script CI uses — OCI, no build; + * # alternatives in docs/e2e-tests/local-e2e-harness.md): + * ./e2e-tests/local-harness/populate.sh + * + * yarn --cwd e2e-tests e2e:legacy-local + * + * Both servers are started via `webServer` with the guest-auth overlay + * `app-config.local-e2e.yaml` plus the dynamic-plugins UI config. An already-running + * pair of servers is reused locally; in CI they are started fresh. + */ + +const frontendUrl = "http://localhost:3000"; +const backendReadiness = "http://localhost:7007/.backstage/health/v1/readiness"; +const repoRootBin = resolve(process.cwd(), "..", "node_modules", ".bin"); +const pathWithRepoBin = `${repoRootBin}:${process.env.PATH ?? ""}`; +const isCI = process.env.CI !== undefined && process.env.CI !== ""; + +// The last entry is generated by e2e-tests/local-harness/populate.sh: it carries the +// pluginConfig blocks from the harness dynamic-plugins.yaml (e.g. the full global-header +// mount points), merged by install-dynamic-plugins exactly like the production container +// does. It must come after the static app-config.dynamic-plugins.yaml so its plugin +// config wins. Re-run populate.sh after changing the harness plugin set. +const sharedConfigArgs = [ + "--config ../../app-config.yaml", + "--config ../../app-config.dynamic-plugins.yaml", + "--config ../../app-config.local-e2e.yaml", + "--config ../../dynamic-plugins-root/app-config.dynamic-plugins.yaml", +].join(" "); + +export default defineConfig({ + testDir: "./playwright", + // Fails fast if dynamic-plugins-root has not been populated. + globalSetup: "./playwright/support/local-harness-global-setup.ts", + // A test runs cluster-free when its spec file is listed in `testMatch` (an + // allowlist, so unvalidated specs are never loaded) AND it carries the + // @cluster-free tag. To widen coverage: tag the test where it lives and add its + // spec file here. Validated so far: the full guest-signin spec (home page via the + // dynamic-home-page OCI plugin; Settings/Sign-out via the global-header OCI plugin + // with its canonical pluginConfig) and the learning-paths spec (static fallback + // data bundled with packages/app). + testMatch: ["e2e/guest-signin-happy-path.spec.ts", "e2e/learning-path-page.spec.ts"], + grep: /@cluster-free/u, + timeout: 90 * 1000, + forbidOnly: isCI, + retries: isCI ? 1 : 0, + // serial: a single shared backend + dev server + workers: 1, + reporter: [ + ["list"], + ["html", { open: "never", outputFolder: "playwright-report-legacy-local" }], + [ + "junit", + { + outputFile: process.env.JUNIT_RESULTS ?? "junit-results-legacy-local.xml", + }, + ], + ], + use: { + baseURL: frontendUrl, + ignoreHTTPSErrors: true, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, + actionTimeout: 15 * 1000, + navigationTimeout: 60 * 1000, + }, + expect: { + timeout: 15 * 1000, + }, + // backstage-cli / janus-cli live in the repo-root node_modules/.bin, which yarn does + // not surface for these workspaces, so both CLIs are invoked directly with the root + // .bin prepended to PATH and run from their package directory. The backend command + // mirrors packages/backend's `start` script (--require instrumentation) — keep in sync. + webServer: [ + { + command: `backstage-cli package start --require ./src/instrumentation.js ${sharedConfigArgs}`, + cwd: "../packages/backend", + env: { + ...process.env, + PATH: pathWithRepoBin, + NODE_OPTIONS: "--no-node-snapshot", + }, + url: backendReadiness, + reuseExistingServer: !isCI, + timeout: 180 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + { + command: `janus-cli package start ${sharedConfigArgs}`, + cwd: "../packages/app", + env: { ...process.env, PATH: pathWithRepoBin }, + url: frontendUrl, + reuseExistingServer: !isCI, + timeout: 240 * 1000, + stdout: "pipe", + stderr: "pipe", + }, + ], +}); diff --git a/e2e-tests/playwright/e2e/guest-signin-happy-path.spec.ts b/e2e-tests/playwright/e2e/guest-signin-happy-path.spec.ts index 2e0a6366ff..ae6bd209fb 100644 --- a/e2e-tests/playwright/e2e/guest-signin-happy-path.spec.ts +++ b/e2e-tests/playwright/e2e/guest-signin-happy-path.spec.ts @@ -27,21 +27,30 @@ test.describe("Guest Signing Happy path", () => { await common.loginAsGuest(); }); - test("Verify the Homepage renders with Search Bar, Quick Access and Starred Entities", async () => { - await uiHelper.verifyHeading("Welcome back!"); - await uiHelper.openSidebar("Home"); - await homePage.verifyQuickAccess("Developer Tools", "Podman Desktop"); - }); - - test("Verify Profile is Guest in the Settings page", async () => { + // @cluster-free: verified green on the cluster-free harness (playwright.legacy-local.config.ts) + test( + "Verify the Homepage renders with Search Bar, Quick Access and Starred Entities", + { tag: "@cluster-free" }, + async () => { + await uiHelper.verifyHeading("Welcome back!"); + await uiHelper.openSidebar("Home"); + await homePage.verifyQuickAccess("Developer Tools", "Podman Desktop"); + }, + ); + + test("Verify Profile is Guest in the Settings page", { tag: "@cluster-free" }, async () => { await uiHelper.goToSettingsPage(); await uiHelper.verifyHeading("Guest"); await uiHelper.verifyHeading("User Entity: guest"); }); - test("Sign Out and Verify that you return to the Sign-in page", async () => { - await uiHelper.goToSettingsPage(); - await common.signOut(); - await uiHelper.verifyHeading(t["rhdh"][lang]["signIn.page.title"]); - }); + test( + "Sign Out and Verify that you return to the Sign-in page", + { tag: "@cluster-free" }, + async () => { + await uiHelper.goToSettingsPage(); + await common.signOut(); + await uiHelper.verifyHeading(t["rhdh"][lang]["signIn.page.title"]); + }, + ); }); diff --git a/e2e-tests/playwright/e2e/learning-path-page.spec.ts b/e2e-tests/playwright/e2e/learning-path-page.spec.ts index e280e7fd7c..276c644266 100644 --- a/e2e-tests/playwright/e2e/learning-path-page.spec.ts +++ b/e2e-tests/playwright/e2e/learning-path-page.spec.ts @@ -21,21 +21,24 @@ test.describe("Learning Paths", { tag: "@layer3-equivalent" }, () => { await common.loginAsGuest(); }); - test("Verify that links in Learning Paths for Backstage opens in a new tab", async ({ - page, - }, testInfo) => { - await uiHelper.openSidebarButton("References"); - await uiHelper.openSidebar("Learning Paths"); - - // Scope to main content area to get only Learning Path links - const learningPathLinks = page.getByRole("main").getByRole("link"); - - for (const learningPathCard of await learningPathLinks.all()) { - await expect(learningPathCard).toBeVisible(); - await expect(learningPathCard).toHaveAttribute("target", "_blank"); - await expect(learningPathCard).not.toHaveAttribute("href", ""); - } - - await runAccessibilityTests(page, testInfo); - }); + // @cluster-free: verified green on the cluster-free harness (playwright.legacy-local.config.ts) + test( + "Verify that links in Learning Paths for Backstage opens in a new tab", + { tag: "@cluster-free" }, + async ({ page }, testInfo) => { + await uiHelper.openSidebarButton("References"); + await uiHelper.openSidebar("Learning Paths"); + + // Scope to main content area to get only Learning Path links + const learningPathLinks = page.getByRole("main").getByRole("link"); + + for (const learningPathCard of await learningPathLinks.all()) { + await expect(learningPathCard).toBeVisible(); + await expect(learningPathCard).toHaveAttribute("target", "_blank"); + await expect(learningPathCard).not.toHaveAttribute("href", ""); + } + + await runAccessibilityTests(page, testInfo); + }, + ); }); diff --git a/e2e-tests/playwright/support/local-harness-global-setup.ts b/e2e-tests/playwright/support/local-harness-global-setup.ts new file mode 100644 index 0000000000..a8e6e2bf0c --- /dev/null +++ b/e2e-tests/playwright/support/local-harness-global-setup.ts @@ -0,0 +1,34 @@ +import { readdirSync } from "fs"; +import { resolve } from "path"; + +/** + * globalSetup for the cluster-free legacy harness (playwright.legacy-local.config.ts). + * + * Fails fast with an actionable message when `dynamic-plugins-root` has not been + * populated — otherwise the legacy app boots with no plugins and specs fail with a + * confusing locator timeout instead of a clear "populate first" error. + */ +export default function requireDynamicPluginsPopulated(): void { + // process.cwd() is e2e-tests when Playwright runs; the plugins root is at repo root. + const root = resolve(process.cwd(), "..", "dynamic-plugins-root"); + + // Plugins are installed as one directory each; count only directories so the + // installer's generated global-config file (written into the same root even when + // zero plugins install) does not satisfy the guard. + let pluginCount = 0; + try { + pluginCount = readdirSync(root, { withFileTypes: true }).filter((entry) => + entry.isDirectory(), + ).length; + } catch { + // root missing — treated as empty below. + } + + if (pluginCount === 0) { + throw new Error( + `dynamic-plugins-root has no plugins — populate it before running e2e:legacy-local:\n\n` + + ` ./e2e-tests/local-harness/populate.sh\n\n` + + `See docs/e2e-tests/local-e2e-harness.md.`, + ); + } +}