Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3eabc3d
UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e rig group
Deepak-Kesavan Oct 20, 2026
e4152d7
UN-4345 [FIX] Keep the port in the Host the e2e frontend proxy forwards
Deepak-Kesavan Oct 20, 2026
8b19b4f
UN-4345 [FIX] Address self-review findings in the v1 E2E harness
Deepak-Kesavan Oct 20, 2026
c1e8972
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Oct 26, 2026
45b82f4
UN-4345 [FIX] Reach the hosted Auth0 form in the v1 harness login and…
Deepak-Kesavan Oct 26, 2026
7f2ce99
UN-4345 [FIX] Keep the runner's overlay manifests out of the rig self…
Deepak-Kesavan Oct 27, 2026
4883888
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Oct 27, 2026
5b51782
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Oct 28, 2026
2d2688a
UN-4345 [FIX] Route v1 harness pages by the organisation's name, not …
Deepak-Kesavan Oct 28, 2026
702186a
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 5, 2026
e611d84
UN-4345 [FIX] Name the env var the role lookup reads, and URI-encode …
Deepak-Kesavan Nov 5, 2026
bde3b84
Merge branch 'main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 9, 2026
76a6b3b
Merge branch 'main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 10, 2026
cc0c058
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 11, 2026
b7e8ba2
UN-4345 [FEAT] Wire the v1 E2E harness to the P0-T03 route registry
Deepak-Kesavan Nov 11, 2026
463bad3
UN-4345 [FIX] Gate on the v1 E2E group, check its types in CI, cover …
Deepak-Kesavan Nov 11, 2026
af85aac
UN-4345 [FIX] Respect distributions in the v1 preview and gate
Deepak-Kesavan Nov 11, 2026
35e59a3
UN-4345 [FIX] Check the distribution grep for each distribution, not …
Deepak-Kesavan Nov 11, 2026
3fba470
UN-4345 [FIX] Assert the premise of the preview placeholder test befo…
Deepak-Kesavan Nov 12, 2026
c89de0b
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 12, 2026
fc0d3c1
UN-4345 [FIX] Let the app record the user in the old-version round tr…
Deepak-Kesavan Nov 12, 2026
4695302
UN-4345 [FIX] Fill a final splat in route patterns the way the fronte…
Deepak-Kesavan Nov 12, 2026
8533666
UN-4345 [FIX] Detect a final splat with endsWith (Sonar S6557)
Deepak-Kesavan Nov 12, 2026
29a5fe0
UN-4345 [FIX] Address Vishnu's review of the v1 E2E harness
Deepak-Kesavan Nov 12, 2026
abe6478
UN-4345 [FIX] Run the v1 suite's typecheck before the image build; do…
Deepak-Kesavan Nov 12, 2026
ef14ef8
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 12, 2026
d721dbc
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 12, 2026
ee47be5
Merge remote-tracking branch 'origin/main' into UN-4345-v1-e2e-harness
Deepak-Kesavan Nov 12, 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
46 changes: 46 additions & 0 deletions .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,34 @@ jobs:
# Cheap pre-flight before the multi-minute image build.
run: tox -e rig -- validate

# Frontend v1 E2E (`frontend-v1-e2e`) runs with `npx --no-install`, so its
# own node_modules and a browser must already be here. Scoped to
# tests/e2e/ui-v1 only; the v0 `ui` group's setup is deliberately left
# as it is. The leg runs the whole e2e tier, so the group is always
# selected; the caches keep that cheap. Install, lint and typecheck run
# before the image build, so a type error fails in seconds; the browser
# install stays after it.
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

- name: Cache frontend-v1 E2E dependencies
uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-ui-v1-${{ hashFiles('tests/e2e/ui-v1/bun.lock') }}

- name: Install frontend-v1 E2E dependencies
working-directory: tests/e2e/ui-v1
# No dependency lifecycle scripts: browsers are installed below.
run: bun install --frozen-lockfile --ignore-scripts

- name: Lint and typecheck frontend-v1 E2E
working-directory: tests/e2e/ui-v1
# Playwright strips types without checking them, and neither the
# frontend lint workflow nor pre-commit covers tests/. The typecheck
# also holds the suite to the frontend registry types it imports.
run: bun run lint && bun run typecheck

- name: Set up env files
run: ./run-platform.sh -e

Expand Down Expand Up @@ -276,6 +304,24 @@ jobs:
worker-unified.cache-from=type=gha,scope=worker-unified
worker-unified.cache-to=type=gha,mode=max,scope=worker-unified

- name: Cache Playwright browsers
id: ui-v1-browsers
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
# The lock pins Playwright, which pins the browser build.
key: ${{ runner.os }}-playwright-ui-v1-${{ hashFiles('tests/e2e/ui-v1/bun.lock') }}

- name: Install Playwright browser
working-directory: tests/e2e/ui-v1
# The OS libraries are not cacheable; on a cache hit install only them.
run: |
if [ "${{ steps.ui-v1-browsers.outputs.cache-hit }}" = "true" ]; then
npx --no-install playwright install-deps chromium
else
npx --no-install playwright install --with-deps --only-shell chromium
fi

- name: Run e2e tier (rig boots the platform via compose)
env:
UNSTRACT_E2E_RUNTIME: compose
Expand Down
18 changes: 15 additions & 3 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,13 @@ Optional knobs (see `groups.yaml` for examples):

### Frontend groups (`vitest` / `playwright`)

Two groups run Node suites rather than pytest:
Three groups run Node suites rather than pytest:

| Group | Tier | Runner | What it covers |
|---|---|---|---|
| `frontend` | unit | `vitest` | Vitest + React Testing Library over jsdom — the antd→shadcn shims and the components on top of them. Workdir `frontend`. |
| `ui` | e2e | `playwright` | Browser e2e against the real UI. Specs live in `tests/e2e/ui/`, beside the other e2e suites. |
| `frontend-v1-e2e` | e2e | `playwright` | Browser e2e for the v1 frontend (epic UN-4257). Specs and harness live in `tests/e2e/ui-v1/`; see its README. |

```bash
python -m tests.rig run frontend # the whole vitest suite
Expand All @@ -135,18 +136,29 @@ cd tests/e2e/ui && npx playwright install chromium
`tests/e2e/ui` is self-contained — specs, `playwright.config.js` and its own
`node_modules` — so it resolves without reaching into `frontend/`.

Both emit **real JUnit**, so `parse_junit` reports per-test counts exactly like
All three emit **real JUnit**, so `parse_junit` reports per-test counts exactly like
a pytest group (unlike `hurl`, which gets one synthetic row). Vitest takes
`--outputFile`; Playwright reads `PLAYWRIGHT_JUNIT_OUTPUT_NAME`, which the rig
exports — that is why the config reads it.

Both are `optional`, deliberately. They shell out via `npx --no-install`, so a
`frontend` and `ui` are `optional`, deliberately. They shell out via `npx --no-install`, so a
machine or CI image without `node_modules` (or without the browser binaries)
**skips** instead of failing the build. The runner returns exit 5 — the rig's
"nothing collected" convention — when `npx` is missing entirely. If a Node
runner dies before its reporter flushes, the rig backfills a synthetic junit so
the failure still shows in the summary rather than vanishing as a missing group.

`frontend-v1-e2e` is **not** optional: it gates the build. CI's e2e job
installs its `node_modules` and browser before the tier runs. To run it
locally, install them first:

```bash
cd tests/e2e/ui-v1 && bun install --frozen-lockfile && bunx playwright install chromium
```

Without them, `npx --no-install` fails and the group is red; with no `npx` at
all it still exits 5 and reads as skipped, like the other Node groups.

**Auth.** The `ui` suite runs against both deployments. `UNSTRACT_AUTH_MODE`
picks the flow, or it auto-detects:

Expand Down
12 changes: 9 additions & 3 deletions tests/compose/frontend-nginx.test.conf
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ http {
# trailing slash so they behave like that file's PathPrefix() rules
# (`/api/v1` itself matches, not just `/api/v1/...`). Keep the list
# in step with those labels.
#
# `Host $http_host`, not `$host`: `$host` drops the port, so Django's
# CSRF same-origin check compares the browser's `Origin`
# (http://localhost:3000) with http://localhost and rejects every
# unsafe request the SPA makes, starting with the org handshake, which
# leaves the app on its "Please wait while we prepare your session" screen.
location /api/v1 {
proxy_pass $backend_upstream$request_uri;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Expand All @@ -72,7 +78,7 @@ http {

location /deployment {
proxy_pass $backend_upstream$request_uri;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Expand All @@ -82,7 +88,7 @@ http {

location /public {
proxy_pass $backend_upstream$request_uri;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e/ui-v1/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
reports/
test-results/
playwright-report/
106 changes: 106 additions & 0 deletions tests/e2e/ui-v1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Frontend v1 E2E (`frontend-v1-e2e`)

Playwright suite for the v1 frontend (epic UN-4257, task P0-T09). It is kept
apart from the v0 suite in `tests/e2e/ui/`: own config, fixtures, lock and
rig group, so v1 results never mix with v0's (D10, constraint 12).

## Run

```bash
# Once: the suite's own dependencies and a browser. The group gates the
# build, so without these the rig reports it red, not skipped.
cd tests/e2e/ui-v1
bun install --frozen-lockfile
bunx playwright install chromium

# Through the rig, as CI runs it (boots the platform, e2e-smoke first).
# The stack needs a frontend image built with VITE_V1_DEV_ROUTES=true, as
# ci-test.yaml builds it; a release image has no demo pages.
tox -e e2e

# Standalone against a running stack
UNSTRACT_FRONTEND_URL=http://localhost:3000 UNSTRACT_BACKEND_URL=http://localhost:8000 bunx playwright test

bun run lint && bun run typecheck
```

| Env | Meaning |
| --- | --- |
| `UNSTRACT_FRONTEND_URL`, `UNSTRACT_BACKEND_URL` | Stack under test (the rig exports both) |
| `UNSTRACT_AUTH_MODE` | `oss` or `auth0`; detected automatically otherwise |
| `UNSTRACT_ADMIN_USER` / `_PASSWORD` | The `admin` role (defaults to the seeded `unstract` user) |
| `UNSTRACT_E2E_<ROLE>_USER` / `_PASSWORD` | Any other role, upper-cased with non-alphanumerics as `_` (`org-admin` reads `UNSTRACT_E2E_ORG_ADMIN_USER`); a test asking for a role without them skips, and fails under the rig, which seeds only the admin |
| `UNSTRACT_E2E_ORG_ID` | The organisation to select; the first one by default |
| `UNSTRACT_DISTRIBUTION` | `oss` (default), `cloud` or `onprem`: tests tagged for it run. Must match the image's `VITE_DISTRIBUTION` (unset builds as oss), which decides the pages it renders |
| `UNSTRACT_E2E_A11Y_IMPACT` | Lowest axe impact that fails a scan; default `serious` |

## Writing a page spec

One spec per page task, named by task id: `specs/P4-T01.spec.ts`. The P8-T01
shared journeys go in `journeys/`. Every test carries a distribution tag: a
test tagged only for another distribution is not collected (nor are its
hooks), and one with no distribution tag (or a mistyped one, `@OSS`) fails.

The stack under test is the E2E image: built with `VITE_V1_DEV_ROUTES=true`
(ci-test.yaml), which registers P0-T03's demo pages and honours the preview
override. A release image ignores both.

```ts
import { DIST, expect, test } from "../fixtures/index.ts";
import { expectAccessible, expectFiveStates, forEachTheme } from "../helpers/index.ts";

test.describe("P4-T01 API Deployments", { tag: DIST.both }, () => {
// Non-final task of a multi-task page: render v1 before registration (D9).
test.use({ v1Preview: ["UN06"] });

test("five states", async ({ page, app }) => {
await expectFiveStates(page, {
open: () => app.goto("api"),
api: "**/api/v1/unstract/*/api/deployment/**",
rest: (p) => p.getByRole("table"),
emptyBody: [],
});
});

test("accessible in every theme", async ({ page, app, seed }) => {
await seed.create("workflow/", { workflow_name: seed.name("wf") });
await forEachTheme(page, async (theme) => {
await app.goto("api");
await expectAccessible(page, { label: theme });
});
});
});
```

| Fixture or helper | Does |
| --- | --- |
| `session`, `app`, `role` | Signs in as a seeded user of a role, selects the org; `app.goto(route)` opens org-scoped routes |
| `seed` | Creates rows through the API and deletes them after the test |
| `v1Preview` | Renders an unregistered page's v1 routes (non-final page tasks) |
| `DIST` / `TAG` | Marks a test as OSS, cloud, on-prem or several |
| `expectFiveStates` | Drives rest, loading, empty, error and no-permission by intercepting the screen's API |
| `expectAccessible` | axe scan, WCAG 2.0/2.1 A+AA, fails at or above the impact level |
| `forEachTheme` | Repeats a check in Light, Dark and Unstract |
| `expectV0RedirectsToV1` | A v0 URL lands on the mapped v1 route |
| `expectOldVersionRoundTrip` | Toggle pages only (Human Review, page 11, and its demo stand-in): old-version preference on and off, for `session.userId` |

## Where the harness meets the frontend

- `contract/registry.ts` reads P0-T03's registry data from the frontend
(`src/v1/app/registry/{types,entries,validate}.ts`, `src/v1/lib/oldVersion.ts`),
which import nothing Vite-specific. `REGISTRY` is what the E2E image renders
as v1: registered pages plus the demo pages. The typecheck in CI holds the
harness to those types.
- `contract/v1-ui.ts` holds the DOM markers: the v1 root
(`[data-ui-version="v1"]`, carrying `data-theme`), and the theme modes and
storage key from `src/v1/lib/theme.ts`.
- `specs/P0-T09.demo.spec.ts` proves the switch on P0-T03's demo pages
(`/:orgName/v1-demo`, `/:orgName/v1-demo-review`). Those pages load no data,
so the five-state helper is proven on `support/state-fixture.ts`.

Still open:

1. **State components** (P1): set `STATE_TEST_IDS` to the core skeleton, empty,
error and no-permission components, following the team's selector decision
for v1 tests.
2. **a11y level** (P8-T02): confirm or change the default `serious`.
28 changes: 28 additions & 0 deletions tests/e2e/ui-v1/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
"root": true,
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"includes": [
"**",
"!node_modules",
"!reports",
"!test-results",
"!playwright-report"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": { "noExplicitAny": "error" },
"style": { "useImportType": "error" }
}
}
}
Loading
Loading