frontend-patterns/research/experiments/boundary-recovery/README.md
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

113 lines
6.5 KiB
Markdown

# 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](../../reviews/BoundaryRecoveryExperiment.md).
## Reproduce
From this directory:
```bash
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](provenance.json); 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](result-immediate.json) records 7/8; [result-settled.json](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](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](../../reviews/NestedDismissalIsolation.md) and [48-trial receipt](isolation-results.json)
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.
```bash
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](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](routing-design.md), [review](../../reviews/NestedOwnershipRouting.md)
and [corrected results](routing-results.json) 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.
```bash
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](routing-development-results.json) used an incorrect
accessibility-exposed dialog count for cancellation. Their original `routing.mjs`
bytes are retained as [routing-development-runner.mjs](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.