fix(ios): cut modal-contained presentations from bridge snapshots - #2850
Conversation
9b8fc13 to
29d4208
Compare
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed at 29d4208. The rule looks right for dimmed sheets, but there is no live evidence yet that it leaves a reachable presenting screen alone. Does Not blocking: CI was still running at review time. Mutants (ios-modal-containment), Repo Guards and Typecheck cover the new module and the mutation config, so a failure there would count against this diff. |
2074bc4 to
40d7216
Compare
|
Answering the live-evidence finding at 1. An undimmed screen is left alone. Root: 2. A dimmed presentation is cut, measured against the same state with the rule disabled. Sequence
The covering container is never a candidate — the loop cuts 3. Dismissal restores reachability, so the cut is per-capture and not destructive. From the modal: back → The PR's risk line is now measured rather than assumed: the modals in this app all carry a dimming view, so the "presentation UIKit does not dim" case is the untouched path in item 1, not a live gap I hit. |
|
40d7216 does not add the evidence the earlier review (29d4208) asked for, so this is still evidence-pending. All 18 checks are green at 40d7216, and there is no failing job to attribute. The open question is whether the cut can fire on a sheet resting at an undimmed detent ( Separately, the mutation lane that 29d4208 added for this rule is gone from the rebased head now that the rule lives in geometry-policy.ts (https://github.com/callstack/agent-device/blob/40d7216/scripts/mutation/modules.ts#L1). Is this rule covered by an existing mutation module, or does one need to be added so a regression here is caught? Not blocking: whether an existing mutation module already covers geometry-policy.ts can be checked and folded in later if it's missing. I could not run the device check myself, so the dimmed-modal cut and the press-navigation result are author-reported. The next thing standing between this and merge is a live bridge capture of a sheet at an undimmed detent showing the presenting screen's controls survive |
The host AX bridge reports every sibling container in a window, so each UIKit modal presentation left the screens it presents over published as live content. A sheet covers only part of the screen, so the geometric occlusion pass left the covered controls actionable and selectors resolved them: a three-screen stack published 567 nodes across three screens where the same app under the XCTest runner published 76 across the top one. The presentation cut now applies modal containment: when the last transition view under a container carries UIKit's dimming view as its direct child, the earlier transition views that dimmed area spans, and their subtrees, are cut from the regular and interactive snapshots. Raw snapshots still report them, and producers that report no UIKit class names never trigger the rule, so it stays inert on the runner. Every removed source is counted in the fold's presentation stats (ADR 0026). The rule joins `geometry-policy.ts`, the module the fold already evaluates for its per-node decisions, rather than arriving as a new eager module: the eager-closure budget holds this entry surface at its merge-base size.
A sheet resting at an undimmed detent keeps UIKit's dimming view as a direct child of its transition view, at the same window-sized frame, while the presenting screen stays reachable. The modal-containment rule treated any such dimming view as proof of modality, so the bridge snapshot cut the presenting screen's controls: on react-navigation's form-sheet example, `snapshot -i` over the `Custom Dimming` sheet published only the sheet, although a press on the presenting `Height Steps` button navigates. The Simulator AX bridge now reads `XC_kAXXCAttributeIsUserInteractionEnabled` for dimming views (source version v1.7.0) and the decoder publishes it as `userInteractionEnabled`. It reads false for the undimmed detent and true for a dimmed form sheet and a card modal; no attribute the bridge already read tells them apart, and `IsVisible` reads false for both the undimmed sheet and the card modal. The read targets dimming views alone: requesting the attribute for every node cost about half again the capture time on a 492-node tree. The rule now asserts containment only when the dimming view takes touches, and fails closed when the producer did not read it. The modal rule and the structural identifier rule now share one parent-link subtree walker in tree.ts.
40d7216 to
31defc4
Compare
|
Addressed at 1. Undimmed detent (blocking). Confirmed, and fixed. I captured the case on the bridge: iPhone 17 Pro, iOS 26.2, I probed extra AX attributes on the dimming view in three states:
Cost on the 492-node card-modal tree, 8 captures each:
Live results at the new head:
2. Negative fixture (non-blocking). Added to 3. One walker (non-blocking). Done. 4. Mutation lane. No Validation. |
|
Reviewed at 31defc4. The earlier conflict is resolved, and the code correctly cuts modal-contained presentations from bridge snapshots. All checks pass and there are no conflicts. The live bridge readings (undimmed detent, dimmed form sheet, card modal) and the Height Steps and Push article results are author-reported, from a run before the walker move. The move reads as behavior-equivalent, so that run still covers the rule. One question: does every UIKit dimming view on iOS 26 report the element type exactly Not blocking: the CHANGELOG's 39-flow Maestro count comes from the 40d7216 run, and the rule was narrowed since. Either re-run the suite at this head or attribute the count to 40d7216. |
|
Summary
The host AX bridge reports every container hanging off a window, so each UIKit modal presentation left the screens it covers published as live content. Maestro's visibility test is geometric, so
tapOnresolved the covered screen's control and flows failed on screens that never arrived;presentation: "formSheet"fields matched intermittently (item 3 of #2638, not closed by this).The presentation cut now applies modal containment: when the last transition view under a container carries UIKit's dimming view as its direct child and that dimming view takes touches, the earlier transition views that dimmed area spans are cut, with their subtrees, from the regular and interactive projections. Containment is asserted only where the producer states it. An undimmed presentation behaves as before. A sheet resting at an undimmed detent also behaves as before: its dimming view reports
userInteractionEnabled: false.snapshot --rawstill reports covered screens. Producers that report no UIKit class names, or no dimming-view interaction state, never trigger the rule. Removals are counted instats.modalContainedNodeCount(ADR 0026). ADR 0004 records the decision and the rejected seams.The Simulator AX bridge (source version v1.7.0) reads
XC_kAXXCAttributeIsUserInteractionEnabledforUIDimmingViewnodes only, with one follow-up read for each dimming view. The decoder publishes it asRawSnapshotNode.userInteractionEnabled. Requesting the attribute for every node made captures about 50% slower on a 492-node tree. The targeted read did not change capture time.The rule is in
geometry-policy.ts, a module the fold already evaluates. The eager-closure budget keepsios-snapshot-engine/index.tsat its merge-base size.Validation
Head
31defc4c14, rebased onfaa69ff784.pnpm check:affected --runon this head: all runnable checks passed (format, lint, typecheck, layering, fallow, build, related Vitest: 860 files, 6739 tests).rne://native-stack-form-sheet→Custom Dimming): dimming views reportuserInteractionEnabled: false.snapshot -ihas all 10 presenting buttons plus the sheet'sClose. Before this fix it hadCloseonly.press 'label="Height Steps"'reached the presenting screen and opened that sheet.Height Steps):userInteractionEnabled: true.-ishows the sheet only (5 nodes).native-stack-card-modal→Push article→Push albums):userInteractionEnabled: true.-ihas 14 nodes, headerAlbums. The nextPush articlepress reachedArticle by The Doctor.userInteractionEnabledcheck kills 2 tests. Treating an unread fact as blocking kills 1 test. Decoder test for true, false, absent and malformed values.40d72169c5): React Navigation's Maestro suite passed with 39 passed (39) on an iPhone 17 Simulator (iOS 26.2). Five hand-applied mutants were each killed by a named test.geometry-policy.ts. None was added in this PR.