Skip to content

test(ui): package-level lifecycle + multi-instance + reconnect semantics (#1227, B2.2) - #1299

Merged
SisyphusZheng merged 1 commit into
devfrom
v044/1227-ui-package-lifecycle-tests
Sep 3, 2026
Merged

test(ui): package-level lifecycle + multi-instance + reconnect semantics (#1227, B2.2)#1299
SisyphusZheng merged 1 commit into
devfrom
v044/1227-ui-package-lifecycle-tests

Conversation

@SisyphusZheng

Copy link
Copy Markdown
Member

Problem

#1227 (stage #1288, Beta.2 / B2.2): re-verify and close two independent-audit findings against @openelement/ui:

  • L5 — missing package-level tests: packages/ui/__tests__ did not cover several package exports' real semantics, and there was no exports-vs-tests accounting.
  • L4 — theme-toggle multi-instance/dual-state lifecycle and reconnect behavior had no package-level evidence: multiple instances on one page must not fight, light/dark transitions must be correct per instance, reconnect after disconnect must re-sync exactly once, dispose must clean up.

B2.1 (#1226, PR #1298) added browser-level qualification (ui-dogfood fixture, 3 engines). This PR adds the package-level semantics layer only — no browser coverage is duplicated; where unit-level cannot observe a semantic, the test file says so and points at the browser evidence instead of fake-passing.

Owner

Beta.2 B2.2 (#1227). One bounded slice: packages/ui/__tests__ only. No production code changed.

Before — exports vs package-level tests

Export Package-level tests before
openPropsTokenSheet / openPropsRootSheet ✅ css-smoke token contract
manifest / registerOpenUi ✅ smoke (tags, idempotent registration) — but no attribute/event contract per declaration
OpenButton ✅ click/form choreography, disabled guard
OpenInput ⚠️ value/events, realm-unique ids, form reset/disabled — no activation-id stability, no focus/blur events
OpenCodeBlock ✅ copy success/failure feedback
OpenBadge ❌ smoke export only (declarative; no imperative semantics exist)
OpenCard ❌ smoke export only (declarative; no imperative semantics exist)
OpenCallout ❌ smoke export only (icon fallback is computed-reactivity — not unit-observable, see below)
OpenThemeToggle ⚠️ init priority + persistence policy only — nothing for multi-instance, dual-state transitions, reconnect, dispose (L4)
OpenDialog ✅ top-layer state machine, close echo — no dispose/effect-teardown, no reopen-session pin
OpenDropdown ✅ popover guard, anchor uniqueness — no reconnect wire-once pin
OpenTabs ⚠️ keyboard pattern only — no decorate/ARIA wiring, no dispose/reconnect

After — what was added

New packages/ui/__tests__/lifecycle.test.ts (17 tests) + 1 manifest-contract test in components.test.ts; the fake-DOM harness was extracted to packages/ui/__tests__/harness.ts (shared, idempotent install) instead of being duplicated.

L4 — open-theme-toggle semantics now pinned (8 tests):

  1. Two instances resolve independently from their own priority chains (attribute vs storage) — per-instance host attributes, per-instance exactly-once propagation, no shared static state.
  2. A later-initializing instance converges to the live document theme over stale storage — the actual mechanism by which multiple toggles on one page agree instead of fighting (this behavior was discovered by the test, then verified against initTheme's priority chain: applyTheme writes documentElement[data-theme], which the next instance's chain reads).
  3. One instance's user toggle never disturbs the other's resolved state; each toggles from its own state.
  4. Dual-state dark→light→dark transitions re-apply all three channels per change: host data-theme attribute, documentElement[data-theme], style.colorScheme; exactly one open:theme-change per actual change.
  5. Re-applying the current theme (attribute echo old === val, repeat applyTheme) dispatches nothing and persists nothing.
  6. Reconnect: initTheme runs its body exactly once per instance — repeated lifecycle scheduling (connect rAF + DSD rAF + reconnect) performs zero re-resolution, zero document rewrites, zero re-dispatch, zero storage writes, even when every resolution source changed while detached.
  7. Dispose: teardown-free by design (no self-registered listeners/effects — the click sink is kernel-owned); base teardown is a safe no-op, the disposed instance stays inert, and a replacement instance initializes fresh and converges to the live document theme.
  8. Attribute-driven theme changes apply but never persist (persistence stays exclusive to the explicit user toggle, chore(ui,create): round-5 low hygiene sweep #804 complement); non-light values resolve to dark.

Dispose/reconnect pins for the other primitives:

  • open-dialog: dispose runs the compiled-effect teardown exactly once and clears the instance-state slot; double-dispose is safe; reconnect re-subscribes. Each open session enters the top layer exactly once (modalActive guard) and reopen after close re-enters exactly once.
  • open-tabs: activation decorates the full WAI-ARIA wiring (role/aria-selected/aria-controls/aria-labelledby/tabindex/hidden pairing); click wiring attaches exactly once per tab across dispose→reconnect; reconnect re-syncs stale ARIA (unit-level analog of the ui-dogfood browser test); two instances get independent id prefixes and selection state.
  • open-dropdown: focus-return listeners (focusin/toggle) attach exactly once across dispose→reconnect; the realm-unique anchor name is never re-assigned.
  • open-input: activation assigns the realm-unique control id once — reconnect keeps it; open-focus/open-blur dispatch composed+bubbling.
  • instance-state: the shared per-host store itself is pinned (initializer once per host, zero cross-host leakage) — the foundation every lifecycle guard builds on.

L5 — package contract: the generated manifest's per-declaration attribute and event names are now asserted against the published contract (the WC Package Protocol surface adapter-vite compiles from); drift fails the suite.

Deliberately NOT unit-tested (would fake-pass), with evidence pointers:

  • computed() reactivity (open-callout icon fallback, open-input inputClass, open-dropdown anchorStyle): uncompiled, @property fields are plain values so computeds snapshot the initializer and never re-derive — verified by probe. Compiled-sink reactivity is covered by the element package's compiled facade/claim suites.
  • ElementInternals channels (setFormValue/setValidity/:state): _internals is a kernel-owned getter (open-element-implementation.ts:232) with no kernel attached in Deno tests. Browser evidence: ui-dogfood ui-form.spec.ts (FormData, valueMissing, fieldset disable/enable), ui-dialog.spec.ts (:state(open/closed)).
  • connectedCallback → rAF → initTheme scheduling: _requestAnimationFrame is kernel/detached-lifecycle owned. Browser evidence: www e2e/theme-system.spec.ts (init priority, toggle, persistence, multiple toggles) and ui-dogfood ui-tabs.spec.ts (disconnect→reconnect effect re-sync on the real claim path).

CI gating: package-level tests already run in CI — the root deno test discovers packages/ui/__tests__ (verified: 4 test files + harness type-checked via deno test --no-run), and the AutoFlow test:coverage:check gate (ci + release tiers, triggers on packages/**) runs the full root test task. No wiring change needed.

Why-not-second-owner

Test-only change inside packages/ui/__tests__. The harness extraction, the new lifecycle suite and the manifest contract test are one coherent unit — splitting them would land either half unverifiable (the new suite needs the shared harness; the harness refactor needs the suite to justify it). No ADR-0122 frozen paths touched; no production code touched.

Evidence

  • cd packages/ui && deno task test47 passed, 0 failed (was 29) — exit 0
  • Root deno task test1810 passed (105 steps) + 150 passed, 0 failed — exit 0
  • deno task pack:dry-run → exit 0; deno task package-artifacts:check → exit 0 (5 packages)
  • deno lint src/ __tests__/ + deno fmt --check (packages/ui) → clean
  • CI discovery: root deno test --no-run lists all 4 packages/ui/__tests__ test files
  • Mutation (RED) evidence — each key pin was proven to fail when its semantic is broken, then the source was restored byte-identical (git status shows only __tests__ changes):
    • open-theme-toggle.tsx initDone guard removed → reconnect re-runs initTheme zero times… FAILED
    • open-dialog.tsx modalActive reset removed → each open session enters the top layer exactly once; reopen re-enters FAILED
    • open-tabs.tsx wiredTabs guard removed → click wiring attaches once per tab across dispose/reconnect… FAILED

Scope

In: packages/ui/__tests__/{harness.ts,lifecycle.test.ts} (new), components.test.ts (harness refactor + manifest contract test). Out: production code (zero diff), browser-level fixtures (B2.1 owns them), new components, Beta.3 formal UI qualification breadth.

Risk

Minimal: additive test files + a mechanical harness extraction from an existing green suite. The harness CustomEvent stub now forwards EventInit (bubbles/composed) — required for the focus/blur assertions; existing theme tests only read detail and are unaffected (all 21 pre-existing component tests still green).

Closes #1227

…ics (#1227, B2.2)

Close independent-audit findings L4/L5 for @openelement/ui:

- L4 theme-toggle: per-instance priority-chain resolution, document-channel
  convergence between instances, dual-state transitions on all three channels
  (host attribute, document data-theme, colorScheme), no-op re-apply, reconnect
  exactly-once (initDone guard), teardown-free dispose with fresh replacement
  init, attribute-change-never-persists (#804 complement).
- Dispose/reconnect pins for dialog (open-effect teardown + top-layer
  re-entry per session), tabs (click wiring once per tab across reconnect,
  stale-ARIA re-sync), dropdown (focus-return wiring once, stable anchor
  name), input (activation id stability, open-focus/open-blur events).
- L5: manifest attribute/event contract test (WC Package Protocol surface);
  shared fake-DOM harness extracted to __tests__/harness.ts.
- Documented not-unit-observable semantics (computed reactivity,
  ElementInternals channels, connectedCallback scheduling) with pointers to
  the ui-dogfood + www browser evidence instead of fake-passing unit tests.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

@SisyphusZheng
SisyphusZheng merged commit fe4d531 into dev Sep 3, 2026
10 checks passed
@SisyphusZheng
SisyphusZheng deleted the v044/1227-ui-package-lifecycle-tests branch September 3, 2026 22:54
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.

1 participant