Skip to content

test(joint-react): specify how many events useOnElementsMeasured delivers - #3520

Open
kumilingus wants to merge 6 commits into
clientIO:devfrom
kumilingus:test/elements-measured-spec
Open

kumilingus wants to merge 6 commits into
clientIO:devfrom
kumilingus:test/elements-measured-spec

Conversation

@kumilingus

@kumilingus kumilingus commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

A specification, written as tests, for when useOnElementsMeasured delivers 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 calls useMeasureElement and registers the node with the store's observer, so it genuinely stays outstanding until a measured size arrives. jsdom's ResizeObserver is a no-op mock, so the measured size is then written with the autoSize option, exactly as the observer pipeline does.

Results

case expected actual
seed pass 1 1
add an element nothing measures 1 1
add an element waiting to be measured, before it is measured 0 1
the same element, after it is measured 1 total 2 total
add two elements nothing measures 1 1
add two elements that both wait 0, then 1 1, then 1
add a plain element and a waiting one together 0, then 1 1, then 1
isInitial reported once 1 1
reset the graph isInitial: true false
reset with elements that wait, before measuring 0 1
isInitial once per reset 1 0
application resizes an element nothing measures 0 1
application sizes an element that is waiting 0, then 0, then 1 when measured 1, then 0, then 1
seed pass with a zero-sized element in the graph 1 1
add a batch containing a zero-sized element 1 1
later batches once a zero-sized element is in the graph 0, then 1 1, then 1

Same 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 isInitial has 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 isInitial is 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 warnResizeOnAutoSizedElement already 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 dev today, 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 unmeasuredElements and nothing ever takes it out, because no measurement is coming — no autoSize write, no markElementRendered. The gate is shut permanently.

This is not hypothetical. In the ci-pipeline-editor/react demo the layout runs 3 times on load with published 4.3.6, 3 times on dev, and 0 times with #3518. GroupEndModel declares size: { width: 0, height: 0 } deliberately; instrumenting deliverMeasurement prints OUTSTANDING end#jobs,end#poll forever.

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

  • The signal the store is missing already half exists: 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.
  • Each event is recorded as just its isInitial flag, not the whole payload, so a failure prints one boolean instead of the entire paper.
  • The probe is mounted beside <Paper>, not inside renderElement. The existing paperRenderElementWrapper puts its children inside renderElement, which mounts one hook instance per element, so adding an element adds an instance and their separate events look like repeats from one subscriber.
  • Jest 30 here, so these can be 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.
  • No changeset: tests only, nothing releasable.
  • Targets dev, which has no CI, so the suite was run locally under both React versions.

🤖 Generated with Claude Code

kumilingus and others added 5 commits September 24, 2026 17:14
…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>
…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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants