frontend-patterns/research/experiments/boundary-recovery
tegwick 29ccad7002 Test lifecycle-aware Escape ownership routing without settling delays
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
2026-09-26 22:58:07 +02:00
..
development-results.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
experiment.mjs Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
fixture.jsx Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
isolation-provenance.json Isolate nested dismissal ownership across browsers and builds 2026-09-26 22:35:12 +02:00
isolation-results.json Isolate nested dismissal ownership across browsers and builds 2026-09-26 22:35:12 +02:00
isolation.mjs Isolate nested dismissal ownership across browsers and builds 2026-09-26 22:35:12 +02:00
package-lock.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
package.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
provenance.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
README.md Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
result-immediate.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
result-settled.json Probe rendering and modal focus boundaries in pinned browser fixture 2026-09-26 22:01:35 +02:00
routing-design.md Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing-development-results.json Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing-development-runner.mjs Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing-fixture.jsx Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing-results.json Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing-transform.mjs Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00
routing.mjs Test lifecycle-aware Escape ownership routing without settling delays 2026-09-26 22:58:07 +02:00

Rendering and focus boundary experiment

Executed 2026-09-26. Real React 19.3.0 / Radix Dialog 1.1.23 in a synthetic browser fixture; no deployed application, provider inference or production requests. Review and interpretation.

Reproduce

From this directory:

npm ci --ignore-scripts --cache /tmp/fep-npm-cache
FEP_CHROMIUM_PATH=/path/to/chrome FEP_RESULT_PATH=/tmp/immediate.json npm test
FEP_CHROMIUM_PATH=/path/to/chrome FEP_NESTED_SETTLE_MS=100 FEP_RESULT_PATH=/tmp/settled.json npm test

Use a locally available Chromium executable, or omit FEP_CHROMIUM_PATH to use Playwright's installed browser. The recorded runs used preinstalled Chromium 151.0.7922.34, not an assertion of compatibility with every browser. Version and executable hash are in provenance; the lockfile pins all packages with archive integrity. Installation does not download a browser automatically here. The runner binds an ephemeral loopback port, bundles the fixture in memory, and closes browser/server in finally. No external network is used by the cases.

Output defaults to /tmp/fep-boundary-result.json; reviewed receipts are not overwritten automatically. A failed obligation makes the runner exit nonzero. The immediate nested-dialog case is known to be timing-sensitive; do not hide it with retries or interpret a later pass as its resolution. FEP_NESTED_SETTLE_MS changes only the pause after nested activation before the first Escape. It is an experimental variable, not a proposed product fix. The browser uses React's development build, without StrictMode. Production builds and other browsers remain untested.

Recorded cases

Case Observation Immediate / 100 ms settled
X01 Render fault uses inner fallback; sibling stays interactive pass / pass
X02 Event-handler exception reaches browser error reporting, not boundary pass / pass
X03 Synthetic GET returns 503; explicit catch sets request-failure state pass / pass
X04 Fault in inner fallback reaches outer boundary pass / pass
X05 Closing modal returns focus to surviving invoker pass / pass
X06 Removing invoker leaves BODY focused with default behavior pass / pass (negative control)
X07 Application close policy selects surviving next-record button pass / pass
X08 Nested close should return to parent trigger, then outer trigger fail / pass

result-immediate.json records 7/8; result-settled.json records 8/8 with identical code/input hashes, except the declared timing parameter. “Pass” means an observation matched its assertion, not that the UX is acceptable: X06 deliberately expects the missing workflow target.

development-results.json preserves three earlier runs in order: 7/8, 8/8, 7/8. They preceded final diagnostic instrumentation; their old runner bytes are not archived, so they are development history, not exact reproducible snapshots. Current-code receipts above retain their matching SHA-256 input hashes. All five runs are reported; no estimate of failure frequency is justified.

Scope

Focus checks inspect activeElement and keyboard behavior, not screen-reader output or full accessibility conformance. X03 handles a response error explicitly; it does not test unhandled promises, timeouts, mutation idempotency or remote rollback. Nested dismissal remains unresolved. Dependency/version compatibility and fixture lifecycle effects must be separated before attributing a library defect.

Follow-up: nested ownership isolation

Review and 48-trial receipt compare development/production, Chromium/Firefox and four explicit variants. The original experiment and receipts above remain unchanged. Follow-up reports 43/48 obligations met, including failures; it is a research runner, not a green CI gate.

PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers node node_modules/playwright/cli.js install firefox
PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers FEP_CHROMIUM_PATH=/path/to/chrome node isolation.mjs

Runs three trials per browser/build/variant, without automatic retries. Default output is /tmp/fep-isolation.json; FEP_RESULT_PATH overrides it. A separate /tmp/fep-isolation-checkpoint.json preserves completed trials if a later launch fails. Launch failures abort; completed matrices retain assertion failures in JSON. A sandbox prevented Firefox accessing its temporary profile here; the recorded completed matrix ran outside that sandbox. Firefox build and limitations are in isolation-provenance.json.

Instrumentation and an experimental guard are applied in memory during bundling; installed vendor files are not modified. The readiness variant observes private listener state and is not a proposed public readiness API. The guard can lose input and is not a validated fix. Do not replace raw failures with readiness passes.

Follow-up: experimental routing

Routing design, review and corrected results compare a single document router with unmodified behavior. Six scenarios across two browsers/builds: routed 24/24, raw 20/24. This is a research patch, not a production repair or published dependency fork.

PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers FEP_CHROMIUM_PATH=/path/to/chrome node routing.mjs

Uses the same installed dependencies/browsers as isolation.mjs. Outputs /tmp/fep-routing.json (override with FEP_RESULT_PATH) and an incomplete checkpoint at /tmp/fep-routing-checkpoint.json. No sleep, readiness wait or automatic retry. The transform changes Escape routing only in memory and verifies the pinned source hash before use. routing-fixture.jsx explicitly controls both dialogs, so its results cannot be pooled with the earlier uncontrolled-inner fixture as matched trials.

The initial development results used an incorrect accessibility-exposed dialog count for cancellation. Their original routing.mjs bytes are retained as routing-development-runner.mjs for hash verification; that archived file is provenance, not the reproduction entry. The corrected runner counts hidden parents and checks listener cleanup. Both runs are retained, and the earlier faulty cancellation scores are excluded from conclusions.