fix(component-tree): make maxNodes honest, and stop offering untappable coordinates - #665
Open
filip131311 wants to merge 1 commit into
Open
fix(component-tree): make maxNodes honest, and stop offering untappable coordinates#665filip131311 wants to merge 1 commit into
filip131311 wants to merge 1 commit into
Conversation
…le coordinates maxNodes promised 'Maximum total nodes to include' but only ever collapsed single-child wrapper chains of length >= 2 — it never dropped a node, so maxNodes:5 returned a 24-node tree. It also fired against a count the renderer does not produce, and its accounting could exceed the tree. Three defects, all measured: Chain discovery pushed every suffix of a chain as its own chain, because the recursion guard read a map that is not populated until the greedy pass below it. The greedy pass could then collapse an outer chain and one nested inside it, counting the shared nodes twice: an 8-node tree reported '11 wrapper nodes collapsed'. Members are now consumed as chains are taken, and the total is measured as the difference between rendered counts rather than accumulated, so it cannot disagree with the tree above it. The inline marker counted the chain start, which is printed directly above it, so a 7-wrapper chain said 'via 7 wrappers' while hiding 6. The budget was computed from a plain subtree count, but the renderer also skips same-name overlapping siblings. Budgeting against the larger number would have shrunk trees that were already within budget. What maxNodes will not do is drop a node carrying a name, text, testID, or a branch. Truncating those is how a caller concludes an element does not exist when it was merely trimmed — a wrong answer, which is worse than an oversized response. When the budget cannot be reached the response says so and by how much, and points at debugger-inspect-element for narrowing. The schema now describes that contract instead of promising a hard cap. Separately: an element whose centre lies outside the viewport was rendered as '(tap: 0.22,-0.11)'. The on-screen filter keeps such nodes deliberately (a 10% margin, so a partially-visible row still appears), which means the DEFAULT response offered coordinates that cannot land, formatted exactly like real ones. Those now read '(off-screen: x,y)' — the numbers stay, since their sign says which way to scroll, but the tap affordance is gone. Finally, the off-screen count is reported even at zero. Suppressing it is why onScreenOnly:true and false look identical on a screen with nothing off-view, which reads as a broken parameter rather than an inactive filter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #631 (the
maxNodeshalf — see below ononScreenOnly).maxNodesConfirmed:
{maxNodes: 5}returned a 24-node tree.maxNodesonly ever collapsed single-child wrapper chains of length ≥ 2 — it never dropped a node. And there's a self-defeating interaction: the TS-side filter already removes content-free wrappers before this runs (Content-free wrapper: 47in the same session), so the nodes it wants to collapse are mostly gone by then.Three separate defects, each measured on a probe rather than read off the source:
1. Accounting could exceed the tree. Chain discovery pushed every suffix of a chain as its own chain, because the recursion guard
if (!collapsed.has(cid))reads a map that isn't populated until the greedy pass below it. The greedy pass then collapsed an outer chain and one nested inside it, counting shared nodes twice:Members are now consumed as chains are taken, and the total is measured as the difference between rendered counts rather than accumulated — so it cannot disagree with the tree printed above it.
2. Off-by-one in the inline marker. The chain start is printed directly above the marker, so a 7-wrapper chain hid 6 but said
via 7 wrappers.3. The budget was computed against a count the renderer doesn't produce.
totalVisiblewas a plain subtree count, butrenderNodealso skips same-name overlapping siblings. Budgeting against the larger number would shrink trees that were already within budget.What it deliberately does not do
It does not drop nodes carrying a name, text, testID, or a branch. Truncating those is how a caller concludes an element doesn't exist when it was only trimmed — a wrong answer, and worse than an oversized response. This matters concretely: the collector emits children before siblings, so
BottomTabBar, footers and sticky CTAs are always in the tail of the tree. Any "cut the last N" strategy systematically deletes the actionable bottom of the screen.So when the budget can't be met, it says so instead:
The schema now describes that contract rather than promising "Maximum total nodes to include".
Verified on a 7-content-node tree at
maxNodes: 2: every named node survives and the shortfall is stated.Untappable coordinates (not in the issue)
An element whose centre is outside the viewport rendered as
(tap: 0.22,-0.11). The on-screen filter keeps such nodes deliberately — it has a 10% margin so a partially-visible row still appears — which means the default response offered coordinates that cannot land, formatted exactly like valid ones:The numbers stay (their sign says which way to scroll); the
tap:affordance goes. Rejected clamping — it converts a detectable error into a silent tap on a different element. Nothing parses this format (verified: only the emitter and prose), so the change is safe; the tool description and the three SKILL references are updated.onScreenOnly— not a bugReproduced the reporter's observation and then disproved the diagnosis; full evidence on the issue. Short version: with genuinely off-screen content,
truereportsOff-screen: 2and omits two nodes whilefalseincludes them at the identical scroll position. The 207 JS-skipped figure the report cites is host components filtered by name (View,RCTView,RCTText), not visibility. There's also a pre-existing passing test,component-tree.test.ts:101, asserting the behaviour.What was real is that you couldn't tell:
Off-screen: Nwas suppressed at zero, so an inactive filter looked identical to a broken one. It's now reported even at zero — one line, and it's the signal the reporter went looking for.Found while here — filed separately
renderNodeskips a same-name overlapping sibling and its entire subtree, including labelled content, with no entry inincludeSkipped. Probe: two siblingCards, the second holdingInner "SECOND-CARD-CONTENT"andSubmit [testID=submit-btn]— both silently absent,--- Filtered ---empty. Bigger wrong-answer risk than either claim here, so it gets its own issue rather than riding along.Checks
maxNodes: 5now explains itself, the off-screen marker appears in the default mode,BottomTabBarpreserved