Skip to content

test(ui): @openelement/ui v0.44 dogfood qualification fixture + CI gate (#1226, B2.1) - #1298

Merged
SisyphusZheng merged 3 commits into
devfrom
1226-ui-dogfood
Sep 3, 2026
Merged

test(ui): @openelement/ui v0.44 dogfood qualification fixture + CI gate (#1226, B2.1)#1298
SisyphusZheng merged 3 commits into
devfrom
1226-ui-dogfood

Conversation

@SisyphusZheng

Copy link
Copy Markdown
Member

Problem

#1226 (stage #1288, Beta.2): qualify @openelement/ui as a real v0.44 dogfood — an external consumer of the qualified framework. The listed complex primitives (dialog, dropdown, tabs, inputs/forms, focus, keyboard, ARIA, state, boundaries, SSR/DSD, claim, reconnect/dispose) had only Deno unit tests against uncompiled sources with stubbed internals — no evidence they work on the real compile → SSR/DSD → serve → browser path. The existing suites could not see whole classes of platform defects (and indeed hid four of them).

Owner

Beta.2 UI dogfood qualification (#1226). One bounded slice: a consumer-style fixture + browser evidence + CI gating, plus minimal defect fixes uncovered by that evidence. Formal broader UI qualification stays Beta.3 per the stage ruling.

Before

  • packages/ui/__tests__ exercised imperative methods against fake HTMLElements; compiled-sink behavior was covered only indirectly.
  • No consumer-style app drove open-dialog/open-dropdown/open-tabs through the real build at all; www dogfoods only button/badge/code-block/theme-toggle/input.
  • Four latent defects (see below) were invisible to every existing suite.

After

New fixture packages/adapter-vite/__fixtures__/ui-dogfood/: a plain app consuming @openelement/ui exactly like an external consumer (packageIslands: ['@openelement/ui'] via the WC Package Protocol manifest, no fixture-private shims, no app shell). All routes are static prerendered, so every page exercises compile → SSR/DSD → static serve → island hydration. 25 Playwright specs run on Chromium, Firefox and WebKit (75 test executions).

CI gating (hard requirement): fixture:ui-dogfood:build|e2e|e2e:browsers|gate deno tasks + a new AutoFlow policy gate fixture:ui-dogfood:gate (ci + release tiers), triggered by packages/ui/**, packages/element/**, packages/adapter-vite/(src|__fixtures__)/**, deno.json — the same three-browser job pattern as fixture:request-time:gate (the autoflow-ci job already installs all three engines).

Per-primitive coverage matrix

Semantic Status Evidence (all Chromium+Firefox+WebKit unless noted)
dialog ✅ covered (+2 defects fixed) e2e/ui-dialog.spec.ts: SSR-open → top-layer :modal at hydration (#1030 choreography), trigger open, focus containment cycle, Escape close, focus return to trigger, exactly-one open-dialog-close, :state(open/closed); e2e/ui-ssr.spec.ts: DSD + shadowrootdelegatesfocus + slotted trigger markup
dropdown/menu ✅ covered as dropdown (popover), 1 defect fixed; full ARIA menu keyboard pattern (arrow-key item nav) — gap → Beta.3 (open-dropdown is a popover toggle, not a menu primitive) e2e/ui-dropdown.spec.ts: per-instance anchor pair, pointerdown/click toggle guard, Escape + outside-click light dismiss, focus return
tabs ✅ covered e2e/ui-tabs.spec.ts: role=tab/tablist/tabpanel, aria-selected/controls/labelledby pairing, roving tabindex, click selection, ArrowLeft/Right wrap + Home/End, focus follows selection
inputs/forms ✅ covered (+1 defect fixed) e2e/ui-form.spec.ts: FormData participation incl. activation-time initial value sync, valueMissing blocks native submission, formResetCallback, formDisabledCallback + fieldset re-enable, disabled drops out of FormData, open-button composed submit
focus management ✅ covered dialog containment + focus return; dropdown focus return; open-input delegatesFocus lands host focus on the inner control
keyboard interactions ✅ covered tabs arrows/Home/End, dialog Escape, dropdown Enter/Escape (WebKit button-Tab keynav difference documented in the spec)
ARIA ✅ covered tabs role/state/relation assertions; dialog aria-label; input aria-invalid/describedby in SSR markup
state ✅ covered :state(open/closed) on dialog, :state(disabled) on input, reflected host attributes
open/light/closed boundaries ✅ covered e2e/ui-boundaries.spec.ts: open (ui primitives expose shadowRoot), light + closed (consumer-authored dogfood-light/dogfood-closed: inline data-oe-light output vs shadowrootmode="closed" encapsulation), plus a no-JS render pass
SSR/DSD ✅ covered e2e/ui-ssr.spec.ts: request-level DSD markup assertions per page; no-JS context proves full render without scripts
claim ✅ covered ui-boundaries.spec.ts claim test: a customElements.define wrapper captures the browser-parsed DSD/light-DOM nodes pre-upgrade; post-hydration the same node references must be live (fresh re-render would replace them)
reconnect/dispose ✅ covered ui-tabs.spec.ts: detach → effect observably off (stale ARIA after property write) → reconnect re-syncs exactly once, no duplicated click wiring

Defects found and fixed (commit a295c9e, each minimal + separately justified)

  1. open-dialog modal close could never exit the top layer. The compiled bool sink removes the inner <dialog>'s open attribute on the same signal before the component sync runs; per the HTML spec, removing the attribute from a modal dialog leaves it :modal and close() is then a no-op (verified against native platform behavior). The close branch now restores the attribute before close(). Also fixed: the native close echo of a programmatic close re-dispatched open-dialog-close (now guarded).
  2. open-dialog :state(open)/:state(closed) never applied in real browsers. The kernel attaches ElementInternals only for form-associated hosts; the dialog now attaches its own internals once (instance-state module). Unit tests had stubbed _internals, hiding this.
  3. open-dropdown focus return silently broken on real pages. Native popover focus return only works when the previously focused element shares the popover's tree; with the host inside a page shadow root, focus dropped to <body> on dismiss. The trigger path records the composed focused element at open time and restores it on close only when the platform failed to (verified against native light-DOM behavior, which keeps working untouched).
  4. open-input could never be re-enabled after a disabled fieldset. formDisabledCallback mirrored onto the host disabled attribute, which the platform counts toward FACE disabledness — the state change never fired again (reproduced with a hand-written FACE). It now mirrors onto the reflect: false property.
  5. delegatesFocus was dropped from DSD serialization (framework, SSR/CSR parity). renderDsd now threads the compiled delegatesFocus static into the template as shadowrootdelegatesfocus; without it, claimed shadow roots lost focus delegation. (packages/element/src/public-runtime.ts — not an ADR-0122 frozen path; freeze:semantics:check passes.)

Unit pins added: dialog close ordering + close-echo guard + input disabled mirroring (packages/ui/__tests__/components.test.ts), DSD delegatesFocus threading (packages/element/__tests__/compiled-server/compiled-composition.test.ts).

Why-not-second-owner

Defect fixes stayed in the same PR deliberately: each is a few lines, each is proven by the fixture evidence added here (the e2e goes red without them), and splitting them would land known-broken primitives on dev behind a dogfood flag that says otherwise. No ADR-0122 frozen paths were touched, so no ADR-0151 amendment citation was required.

Evidence (all local runs reproduced as CI gates)

  • deno task fixture:ui-dogfood:gate → exit 0, 75 passed (25 tests × Chromium/Firefox/WebKit)
  • deno task test → exit 0 (1792 + 150 passed, 0 failed)
  • deno task pack:dry-run → exit 0; package-artifacts:check → PASS (inside autoflow:ci)
  • deno task autoflow:ci (full local CI replica, real run): every gate PASS except test:e2e, which fails only on the 4 mobile visual-baseline screenshots (CJK font rasterizer drift, ratio 0.06 vs 0.05 allowance) — verified pre-existing on clean origin/dev on this macOS host (same 4 fail identically from a pristine worktree build); Linux CI uses the canonical baselines. fixture:ui-dogfood:gate runs and PASSes inside autoflow:ci; dry-run selection lists it for this diff.
  • deno fmt --check / deno lint / deno task typecheck → clean; fixture-local fmt/lint/check clean (the vite.config esbuild.jsx typecheck note matches the request-time fixture's pre-existing pattern and is not CI-gated).

Scope

In: new fixture, e2e specs, deno tasks, one autoflow policy gate, the five defect fixes with unit pins. Out: packages/ui redesign, new components, Beta.3 formal qualification breadth, www changes. Note: the fixture defaults to e2e port 4197 (4190 is occupied/misbehaving on some dev hosts).

Risk

Low: additive fixture + test-only wiring; the production-code delta is five small, targeted fixes each with browser + unit evidence. Watch-items: visual-baseline host drift (pre-existing), and the new gate adds ~30s to the CI matrix on ui/element/adapter changes.

Closes #1226 (all listed semantics covered or explicitly gapped above with Beta.3 pointers).

DevBot added 2 commits September 4, 2026 04:55
…on (#1226)

- open-dialog: the compiled bool sink can remove the inner dialog's `open`
  attribute before the component's sync runs; per the HTML spec, removing the
  attribute from a modal dialog leaves it :modal and close() is then a no-op,
  so the modal session never exits the top layer. The close branch now
  restores the attribute before close(). Also, the native close echo of a
  programmatic close no longer re-dispatches open-dialog-close.
- open-dialog: :state(open)/:state(closed) never applied in real browsers —
  the kernel attaches ElementInternals only for form-associated hosts, so the
  dialog attaches its own once via the instance-state module.
- open-dropdown: native popover focus return only works when the previously
  focused element shares the popover's tree; on real pages (host inside a
  page shadow root) focus dropped to <body> on dismiss. The trigger path now
  records the composed focused element at open time and restores it on close
  only when the platform failed to.
- open-input: formDisabledCallback mirrored onto the host `disabled`
  attribute, which the platform counts toward disabledness — a
  fieldset-disabled control could never be re-enabled. It now mirrors onto
  the (reflect: false) property.
- element: renderDsd threads the compiled delegatesFocus static into the DSD
  template as shadowrootdelegatesfocus; without it, claimed shadow roots
  silently lost focus delegation (SSR/CSR parity).

Covered by new unit pins in packages/ui/__tests__/components.test.ts and
packages/element/__tests__/compiled-server/compiled-composition.test.ts, plus
the ui-dogfood browser evidence in the follow-up commit.
… + CI gate (#1226)

Adds packages/adapter-vite/__fixtures__/ui-dogfood: an app consuming
@openelement/ui as an external consumer (packageIslands via the WC Package
Protocol manifest, plain page markup, no fixture-private shims). Every route
is static prerendered, so each page exercises the real compile -> SSR/DSD ->
serve -> hydrate path.

Playwright evidence (Chromium + Firefox + WebKit):
- open-dialog: SSR-open attribute -> top-layer modal choreography, trigger
  open, focus containment, Escape close, focus return, single-event close,
  :state(open)/:state(closed).
- open-tabs: WAI-ARIA roles/states/relations on live light-DOM children,
  roving tabindex, click selection, ArrowLeft/Right wrap + Home/End with
  focus following selection, reconnect/dispose effect teardown and re-sync.
- open-dropdown: anchor-pair assignment, trigger toggle with the
  pointerdown guard, Escape/outside-click light dismiss, focus return.
- open-input/open-button: FormData participation (incl. activation-time
  initial value), valueMissing blocking, formResetCallback,
  formDisabledCallback + fieldset re-enable, delegatesFocus, per-input
  events.
- boundaries: shadow-open/light/shadow-closed contracts side by side,
  incl. a no-JS render pass.
- claim: customElements.define-wrapped node-identity proof that hydration
  claims the parsed DSD/light DOM instead of re-rendering.

CI wiring: fixture:ui-dogfood:{build,e2e,e2e:browsers,gate} deno tasks plus
an autoflow policy gate (ci + release tiers) triggered by packages/ui,
packages/element, adapter-vite src/fixtures, and deno.json — the same
three-browser CI job pattern as fixture:request-time:gate.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

Comment thread packages/adapter-vite/__fixtures__/ui-dogfood/e2e/server.ts Fixed
The fixture-local deno.json excludes e2e/ from fixture-rooted invocations
(same pattern as the request-time fixture), but repo-root deno fmt/lint walk
the whole tree under root config. The specs were only ever checked
fixture-locally, so they drifted; run 33808083057 caught it.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying openelement with  Cloudflare Pages  Cloudflare Pages

Latest commit: 557e7b3
Status: ✅  Deploy successful!
Preview URL: https://c53bb04d.lessjs.pages.dev
Branch Preview URL: https://1226-ui-dogfood.lessjs.pages.dev

View logs

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

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