Skip to content

UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group - #2355

Open
Deepak-Kesavan wants to merge 9 commits into
mainfrom
UN-4345-v1-e2e-harness
Open

Deepak-Kesavan wants to merge 9 commits into
mainfrom
UN-4345-v1-e2e-harness

Conversation

@Deepak-Kesavan

@Deepak-Kesavan Deepak-Kesavan commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds the frontend v1 E2E harness (P0-T09, UN-4345) and the frontend-v1-e2e rig group, so every v1 page task can test itself in a real browser and report apart from v0.

Built ahead of its blocker. P0-T09 is formally blocked by P0-T03 (version switch and route registry, lane A, UN-4271), and neither P0-T03 nor P0-T01 is on main yet. P0-T09 gates 24 page tasks across 10 lanes, so this PR builds every part that does not need P0-T03. The parts that do are coded against P0-T03's published contract, behind one adapter, and their self-tests skip with a reason.

Why

The page definition of done needs E2E, five states, Light/Dark/Unstract and a clean accessibility scan for every page, reported under frontend v1 and never mixed into v0 results (constraint 12, D10).

How

  • tests/e2e/ui-v1/: a self-contained suite, the same pattern as tests/e2e/ui. It has its own package.json + bun.lock, playwright.config.ts, a strict tsconfig.json and biome.json. It is all TypeScript and imports nothing from tests/e2e/ui/.
  • Fixtures (fixtures/):
    • session/app/role: login as a seeded user of a given role (OSS mock-login, or auth0's hosted form) plus org selection.
    • seed: rows created through the org-scoped API and deleted after the test. A leak fails the test.
    • v1Preview: renders an unregistered page.
    • DIST/TAG: distribution tags. The config runs only tests tagged for UNSTRACT_DISTRIBUTION.
  • Helpers (helpers/):
    • expectFiveStates: rest, loading, empty, error and no-permission by intercepting the screen's API. A held response is always released.
    • expectAccessible: @axe-core/playwright, WCAG 2.0/2.1 A+AA.
    • forEachTheme: theme matrix with a pluggable setTheme.
    • expectV0RedirectsToV1
    • expectOldVersionRoundTrip: page 11 only.
  • Adapters:
    • contract/registry.ts is the only place the harness reads P0-T03 from: the registry, the old-version preference key and the preview mechanism. All three are stubbed.
    • contract/v1-ui.ts holds the assumed DOM markers.
  • Demo specs/P0-T09.demo.spec.ts:
    • Part 1 runs on the live platform: login, seed a workflow, see it on the workflows page, teardown. The page is v0, so its a11y scan is report-only.
    • Part 2 is a stand-in for P0-T03's dev-only demo route: a page.route stub that follows the v1-ui markers. It runs five states, an enforcing a11y scan and the theme matrix. It proves the helpers, not the version switch.
  • Rig: frontend-v1-e2e in tests/groups.yaml (requires_platform, depends_on: [e2e-smoke], optional: true for now). The ui group is untouched.
  • CI (ci-test.yaml, e2e job): Bun, a cached bun install --frozen-lockfile in tests/e2e/ui-v1, and a cached Playwright Chromium. The leg runs the whole e2e tier, so the group is always selected and the caches keep that cheap. The ui group's CI is unchanged.
  • New dependencies (scoped to tests/e2e/ui-v1, frontend/ untouched, exact pins, all over two weeks old): @axe-core/playwright 4.13.0, @playwright/test 1.62.1 (same as ui), @biomejs/biome 2.3.13 and typescript 7.0.2 (both match frontend's lock), @types/node 22.20.3.

Needed from P0-T03 (lane A)

  1. The registry module must be importable outside Vite: pure data with plain relative imports, no import.meta.env, no tsconfig path aliases, no asset imports. Playwright transpiles TS itself and applies none of those. Emitting JSON is an alternative.
  2. P0-T03 must provide a dev/test-only override (e.g. a build flag, an env, or a localStorage key honoured only in non-production builds) to render an unregistered v1 page. Without it, no non-final page task can E2E itself before go-live, because under D9 only a page's final task registers it.

Open questions

  • a11y level: the scan fails at serious and above by default (UNSTRACT_E2E_A11Y_IMPACT overrides it). The DoD says "accessibility scan clean"; P8-T02 owns the final level. @deepak to confirm.
  • Assumed markers until P0-T01/P0-T04/P1 publish theirs: v1 root [data-ui-version="v1"], state test ids v1-state-*, and theme hook data-theme + class on <html>.

Flip when P0-T01 / P0-T03 land

  1. contract/registry.ts: re-export the real registry and set REGISTRY_SOURCE = "frontend". The redirect and old-version self-tests then run.
  2. contract/registry.ts: the real OLD_VERSION_PREFERENCE key.
  3. contract/registry.ts: V1_PREVIEW set to P0-T03's override, plus the demo key/route in specs/harness/preview.spec.ts.
  4. specs/P0-T09.demo.spec.ts part 2: point at the real dev-only demo route, then delete support/demo-page.ts.
  5. contract/v1-ui.ts: the real v1 root marker and the core state component ids (P1). The P0-T01 scaffold (UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380) renders no DOM: frontend/src/v1/index.ts is export {} until the version switch (UN-4271, P0-T03) mounts v1. So the marker comes from that mount, and [data-ui-version="v1"] stays assumed until then.
  6. helpers/themes.ts: defaultSetTheme set to P0-T04's mechanism. P0-T04 puts data-theme on the v1 root, not on <html>, and adds a #v1-portal. The default theme hook then targets the v1 root, and the theme and a11y checks also cover #v1-portal, so overlays rendered there are themed and scanned.
  7. a11y default level once agreed (P8-T02).
  8. Drop optional: true on frontend-v1-e2e once CI shows it green.

Can this PR break any functionality?

No app code changes. The new group is optional. Two shared changes:

  • The e2e CI job gains install steps.
  • The test-only nginx config forwards the port in Host (above). That changes behaviour only in the compose test overlay and only lets requests through that were wrongly rejected.

tests/e2e/ui and the ui group are unchanged.

Related

  • Spec: P0-T09, contract: P0-T03
  • Cloud mirror: Zipstack/unstract-cloud#1866 (merge after this PR)

How to test

Through the rig, exactly as the CI e2e job runs it:

cd tests/e2e/ui-v1 && bun install --frozen-lockfile && bunx playwright install chromium && cd -
tox -e e2e                    # whole e2e tier, as CI
# or only this group against a stack you already run:
UNSTRACT_FRONTEND_URL=http://localhost:3000 UNSTRACT_BACKEND_URL=http://localhost:8000 \
  bunx --cwd tests/e2e/ui-v1 playwright test

Local run (compose runtime, latest images, an isolated project on shifted ports):

Status Group Passed Failed Skipped
✅ e2e-smoke 2 0 0
❌ ui 0 1 0
✅ frontend-v1-e2e 17 0 2
✅ e2e-workflow / e2e-prompt-studio / e2e-login / e2e-coowners / e2e-api-deployment 8 0 0
❌ e2e-etl 0 1 0
  • frontend-v1-e2e reports as its own group. The 2 skips are the registry-driven check and the preview self-test, both waiting on P0-T03 (each skip carries its reason).
  • ui is red locally only because it has no node_modules here, the same as in CI. It is optional and untouched by this PR.
  • e2e-etl failed an ETL execution on the published latest images. This PR has no backend change, and main's CI is green.
  • Running tox -e groups -- frontend-v1-e2e alone also pulls in unit-sdk1 as a dependency. That group fails locally because the rig exports UNSTRACT_LLM_MOCK_RESPONSE for any selection with a platform group, which leaks into the sdk1 streaming tests. This is pre-existing and not touched here; tox -e e2e, like CI, avoids it.

Also fixed: the e2e frontend proxy rejected every SPA write

tests/compose/frontend-nginx.test.conf forwarded Host $host, which drops the port. Django's CSRF same-origin check then compared the browser's Origin: http://localhost:3000 with http://localhost and rejected every unsafe request: CSRF Failed: Origin checking failed. The SPA's org handshake (POST /organization/<id>/set) got a 403, and the app sat on "Please wait while we prepare your session" forever. It is now $http_host. This affected the v0 ui suite too; it went unnoticed because ui never had its dependencies installed in CI.

Checklist

  • Self-reviewed (automated review-pr pass, own mode)
  • Biome + tsc --noEmit clean in tests/e2e/ui-v1
  • CI e2e leg green. Needs the PR marked ready or a manual run; the e2e job skips drafts.

Adds tests/e2e/ui-v1: a self-contained Playwright suite (own config,
lock, Biome, strict TS) with login/org, seed, v1Preview and distribution
tag fixtures, and five-state, accessibility, theme-matrix, redirect and
old-version helpers. P0-T03 dependencies go through contract/registry.ts
(stubbed; built ahead of P0-T03). Adds the frontend-v1-e2e rig group and
the CI install steps for it. tests/e2e/ui and the ui group are untouched.
$host drops the port, so Django's CSRF same-origin check compared the
browser's Origin (http://localhost:3000) with http://localhost and
rejected every unsafe request the SPA made. The org handshake failed and
browser-driven specs never got past the session loader.
- five-states: don't await open() while the loading response is held, so
  an open that waits for networkidle (app.goto) cannot deadlock; hand
  unstubbed rest to route.fallback() so a spec's own routes still apply.
- seed: a row created but not resolvable to a delete URL fails teardown.
- old-version: assert v0 rendered only after the SPA has mounted.
- Self-tests for the impact threshold, the networkidle open, the
  pass-through path and that a custom setTheme replaces the default.
@greptile-apps

greptile-apps Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge with its documented temporary stubs and optional test group.

Summary

Adds a separate frontend v1 browser-test suite and the frontend-v1-e2e rig group.

  • V1 page specs can check screen states, accessibility, and themes in a browser.
  • V1 page specs can use org-scoped sessions, test data, and route checks.
  • The rig runs v1 browser tests as a separate optional e2e group.
  • The test proxy keeps the browser's port when forwarding requests to Django.

Deepak-Kesavan explicitly documents the registry and preview stubs, assumed UI markers, temporary accessibility threshold, and optional group as intentional while the blocking frontend tasks land.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  CI["E2E CI job"] --> Install["Install suite and Chromium"]
  Install --> Rig["Test rig"]
  Rig --> Smoke["e2e-smoke"]
  Smoke --> Suite["frontend-v1-e2e"]
  Suite --> Live["Live login, workflow seed, and cleanup"]
  Suite --> Demo["Stand-in page: states, themes, accessibility"]
  Suite --> Pending["Registry and preview checks: skipped pending P0-T03"]
Loading

Reviews (4) · Last reviewed commit: "UN-4345 [FIX] Route v1 harness pages by ..." · Reviewed by Greptile

Comment thread tests/e2e/ui-v1/fixtures/auth.ts Resolved
… clear the Sonar findings

loginAuth0 now starts where the Login button does (/api/v1/login on the
app origin, with the app as Referer so the callback returns there), bounds
every IdP wait and gives the session fixture its own budget, so a failed
sign-in skips or errors with its reason instead of a generic timeout.

Sonar: drop the redundant RoutePattern alias, keep the stub registry from
being a provably-empty literal, String.raw and \w in the regex helpers,
no nested template, reverse cleanups in place with CSRF headers read once,
accept only string or number ids, explicit assertions in the a11y
self-tests, and NOSONAR on the sequential test.step loops.
…-tests

Cloud CI exports UNSTRACT_RIG_EXTRA_MANIFESTS for the whole job, so
load_groups() merged groups.cloud.yaml into the synthetic manifests the
test_cli tests patch in. The first cloud requires_platform group
(frontend-v1-e2e-cloud, which must depend on e2e-smoke) then failed
validation against manifests that never define e2e-smoke. Clear the
variable per test; the overlay tests set it themselves.
Comment thread tests/e2e/ui-v1/fixtures/auth.ts Resolved
…its id

The frontend's /:orgName segment is organization.name from the
/organization/<id>/set response (useSessionValid), which differs from
the id on cloud. The session now carries both: orgId for API calls,
orgName for app.goto and the registry route checks. A harness self-test
pins id != name. Also note that the P0-T01 scaffold renders no v1 root
yet, so the marker stays assumed.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
⚪ e2e-agent-kv e2e 0 0 0 1 0.0
✅ e2e-api-deployment e2e 3 0 0 0 15.5
✅ e2e-coowners e2e 1 0 0 0 0.9
✅ e2e-etl e2e 1 0 0 0 17.6
✅ e2e-login e2e 2 0 0 0 0.7
✅ e2e-prompt-studio e2e 1 0 0 0 14.5
✅ e2e-smoke e2e 2 0 0 0 0.7
✅ e2e-workflow e2e 1 0 0 0 15.3
✅ frontend unit 629 0 0 0 14.1
✅ frontend-v1-e2e e2e 20 0 0 2 11.4
✅ integration-backend integration 1448 0 0 26 57.0
✅ integration-connectors integration 1 0 0 7 7.1
✅ integration-workers integration 181 0 0 1 39.8
❌ ui e2e 0 1 0 0 0.0
✅ unit-agent-kv-schema unit 143 0 0 0 1.4
✅ unit-backend unit 2052 0 0 1 47.5
✅ unit-connectors unit 82 0 0 0 11.8
✅ unit-core unit 281 0 0 0 2.7
✅ unit-filesystem unit 1 0 0 0 3.6
✅ unit-platform-service unit 18 0 0 0 2.6
✅ unit-rig unit 120 0 0 0 3.6
✅ unit-runner unit 12 0 0 0 2.6
✅ unit-sdk1 unit 878 0 0 0 36.0
✅ unit-tool-sidecar unit 8 0 0 0 1.8
✅ unit-workers unit 1534 0 0 2 120.7
TOTAL 7419 1 0 40 428.9

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@jaseemjaskp jaseemjaskp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small things from a review pass over the harness. Overall it looks solid: seed teardown, the held-response release in the five-states helper, the $http_host fix and the CI caching all checked out.

const creds = credentialsFor(role);
test.skip(
!creds,
`no credentials for role "${role}": set UNSTRACT_E2E_${role.toUpperCase()}_USER and _PASSWORD`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The env var named here doesn't match the one credentialsFor() reads. That function sanitises the role (role.toUpperCase().replace(/[^A-Z0-9]/g, "_"), line 47), this message only uppercases it. For role: "org-admin" the skip says to set UNSTRACT_E2E_ORG-ADMIN_USER, but the lookup is UNSTRACT_E2E_ORG_ADMIN_USER, so following the hint never takes effect.

Suggest pulling the key into one helper used by both:

/** The `<ROLE>` in UNSTRACT_E2E_<ROLE>_USER / _PASSWORD. */
function roleEnvKey(role: Role): string {
  return role.toUpperCase().replace(/[^A-Z0-9]/g, "_");
}

and set UNSTRACT_E2E_${roleEnvKey(role)}_USER and _PASSWORD here.

export interface Session {
role: Role;
mode: AuthMode;
/** For API calls: `/api/v1/organization/<orgId>/...`. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: org-scoped API calls go to /api/v1/unstract/<orgId>/... (as seed.ts builds its prefix); /api/v1/organization/<id> is only the list/set handshake. As written this could point spec authors at the wrong path.

Suggested change
/** For API calls: `/api/v1/organization/<orgId>/...`. */
/** For API calls: `/api/v1/unstract/<orgId>/...`. */

const value = JSON.stringify(pages);
if (mechanism.kind === "cookie") {
await context.addCookies([
{ name: mechanism.name, value, url: FRONTEND_URL },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not urgent since V1_PREVIEW is null today, but worth settling when P0-T03 defines the mechanism: for the cookie kind, the value is raw JSON (["UN13"]). Double quotes and commas aren't valid in a cookie-value under RFC 6265, and some server-side parsers mangle them. If the mechanism ends up being a cookie, it may be worth agreeing on an encoding (e.g. encodeURIComponent, or a comma-free format) on both sides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants