test(joint-react): specify how many events useOnElementsMeasured delivers - #3520
Open
kumilingus wants to merge 6 commits into
Open
kumilingus wants to merge 6 commits into
kumilingus wants to merge 6 commits into
Conversation
…vers
Describes the behaviour the hook needs for its stated purpose, running a
layout once element sizes are known: one settled change delivers exactly
one event, where settled means every element in the graph has a size.
Four of the eight cases fail today. All four involve an element added
without a size: the event arrives while the element is still at its
`{0,0}` default, and a second one arrives after it is measured. A layout
driven by this hook therefore runs twice, the first time on an element
with no size.
No fix here. The tests are the specification.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reset replaces the diagram, so the next pass is that diagram's first one: a consumer that fits the paper on `isInitial` has new contents to fit. Today the flag never comes back, because the hook only restarts its history when the effect re-runs, and a reset leaves the paper and the graph identity untouched. Also records only the asserted part of each payload, so a failure prints two booleans rather than the whole paper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whether a size written by the application deserves an event depends on what it does to the graph, not on who wrote it. Resizing an element that already has a size changes nothing about readiness, so it must stay silent: that is the re-entrant layout of clientIO#3514. Sizing an element that had none is the write that makes the graph settled, so it is the event a consumer is waiting for. Pinning both keeps a fix from satisfying one by breaking the other. Suppressing every application resize, as clientIO#3518 does, silences the second one and leaves the graph fully sized with nobody told. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The harness rendered every element as a plain `<rect>`, which nothing measures. Elements it called unmeasured were therefore never pending, just zero-sized, and the cases expecting no event on add were asserting against an element that had nothing to wait for. Elements now render by what they are. A plain one renders as a `<rect>` and is settled on arrival whatever its size. A pending one renders through `<HTMLHost>`, which calls `useMeasureElement` and registers the node with the store's observer, so it genuinely stays outstanding until a measured size arrives. Adds the mixed batch this makes expressible: a plain element and one that waits, added together, are one event, delivered when the second is measured, not one event each. Flushing now waits a macrotask rather than a microtask, so a newly added element's portal has mounted and registered before anything is asserted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A resize does not discharge a pending measurement. The element stays registered, the measured size overwrites the write, and the library already warns about exactly this in `warnResizeOnAutoSizedElement`. So an element waiting to be measured stays outstanding whatever size it happens to hold: what the hook waits on is the measurement, not the presence of a size. That makes both application cases agree. Either the element is settled already, so nothing about readiness changed, or it is waiting, so the measurement is still owed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
samuelgja
approved these changes
Sep 25, 2026
…livery An element can be zero-sized on purpose — a layout anchor marks a position without drawing anything, and its size is its real size, not a size it is waiting for. Nothing ever measures it, so nothing ever reports a size for it, and a gate that waits for one never opens again. Three cases: the seed pass, the batch that adds such an element, and the batches that follow it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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.
Description
A specification, written as tests, for when
useOnElementsMeasureddelivers an event. No fix. Ten of the sixteen cases fail on purpose.The hook exists so an application can run a layout once element sizes are known. That only works if one settled change delivers exactly one event: a layout must not run while an element is still waiting to be measured, and it must not run several times for the same change.
An element is pending only when something is actually going to measure it. That is real here, not simulated. A plain element renders as an SVG
<rect>, nothing measures it, and it is settled the moment it is added whatever its size. A pending element renders through<HTMLHost>, which callsuseMeasureElementand registers the node with the store's observer, so it genuinely stays outstanding until a measured size arrives. jsdom'sResizeObserveris a no-op mock, so the measured size is then written with theautoSizeoption, exactly as the observer pipeline does.Results
isInitialreported onceisInitial: truefalseisInitialonce per resetSame result under React 19 and React 18.
Most failures are one defect. The store bumps its measure state whenever any element has a size, rather than when nothing is left outstanding. So a consumer is told sizes are ready while a newly added element is still waiting, and told again once it arrives. A layout driven by this hook therefore runs twice, the first time on an element with no size.
The mixed batch is the case that matters most in practice. A plain element and one that waits, added together, is one change and should be one event, delivered once the second is measured.
A graph reset has the same shape plus one of its own. A reset replaces the diagram, so the next pass is that diagram's first one and a consumer that fits the paper on
isInitialhas new contents to fit. The flag never comes back, because the hook only restarts its history when the effect re-runs, and a reset leaves both the paper and the graph identity untouched.That is also why
isInitialis not the signal it looks like. It means at least one element has a size, not that measurement has finished.Sizes written by the application
A size the application writes never produces an event, in either situation. If the element is settled already, nothing about readiness changed, which is the re-entrant layout of #3514. If it is waiting to be measured, the measurement is still owed and will overwrite the write, which
warnResizeOnAutoSizedElementalready warns about.Both cases are pinned so that a fix cannot satisfy one by breaking the other. The second also pins the shape of the predicate: after the write the element has a positive size, so a store that gates on "every element has a size" would report settled while the measurement is still outstanding.
An element that stays zero-sized
Zero is a legal final size. A layout anchor marks a position without drawing anything, so nothing measures it and nothing will ever report a size for it. Zero size is not the same as waiting to be measured, and a gate that treats it as such never opens again.
The three cases pin that from the other side of the same predicate: a zero-sized element must not suppress the seed pass, the batch that adds it, or any batch after it. All three pass on
devtoday, apart from the third failing on the shared pending-element defect above.Relationship to #3518
I re-ran the spec with #3518 applied.
It fixes the pending-element and reset failures, which is what it is for. It breaks one case: add a batch containing a zero-sized element goes from 1 event to 0, and stays at 0 for every batch after it.
The cause is that #3518 infers "waiting to be measured" from the size value: a zero-sized element enters
unmeasuredElementsand nothing ever takes it out, because no measurement is coming — noautoSizewrite, nomarkElementRendered. The gate is shut permanently.This is not hypothetical. In the
ci-pipeline-editor/reactdemo the layout runs 3 times on load with published 4.3.6, 3 times ondev, and 0 times with #3518.GroupEndModeldeclaressize: { width: 0, height: 0 }deliberately; instrumentingdeliverMeasurementprintsOUTSTANDING end#jobs,end#pollforever.The gate needs a positive signal that a measurement is actually coming — the element registered a measurer — not an inference from the size value.
Notes
observer.has(id)is true once an element has registered for measurement, and it is used today only for a dev warning. The gap is the window between a cell being added and its portal mounting, during which an element that will be measured has not registered yet.isInitialflag, not the whole payload, so a failure prints one boolean instead of the entire paper.<Paper>, not insiderenderElement. The existingpaperRenderElementWrapperputs its children insiderenderElement, which mounts one hook instance per element, so adding an element adds an instance and their separate events look like repeats from one subscriber.it.failing(...)instead if you would rather the suite stayed green and flipped when the behaviour is fixed. Left as plain failures since the point is to make the gap visible.dev, which has no CI, so the suite was run locally under both React versions.🤖 Generated with Claude Code