[WPT] nested percentage max-height can retain stale constraints after flex cross-size becomes definite
System Info
- Pixel 10 Pro, Android 17; observed device scale 2.625; 1080×2410 comparison images.
- Lynx UIApp package versionName
2.7.0is the shell version, not a verified engine version. - Inspected public source:
d698de373c028a3edf9abf244d7393c2743d7507. The relevant flex algorithm, base layout algorithm, BoxInfo and constraint source match the inspected checkout. The APK's precise engine revision and compatibility configuration remain unverified.
Details
Test and property roles
Source-pinned WPT:
Case 003's relevant structure and CSS:
<div class="outerFlex">
<div class="innerFlex">
<div class="block"><div class="tall"></div></div>
</div>
</div>* { box-sizing: content-box; }
.outerFlex {
display: flex;
width: 100px;
max-height: 100px;
/* default align-items: stretch */
}
.innerFlex {
display: flex;
width: 100px;
background: red;
align-items: flex-end;
}
.block {
width: 100px;
max-height: 100%;
background: green;
}
.tall { height: 9999px; }The full source also has an instruction paragraph and body { overflow: hidden }. Case 004 moves max-height: 100px from the outer flexbox to the inner flexbox.
- Both containers are row flexboxes, so the relevant cross axis is vertical.
- The maximum, not a declared height, limits the auto-height container to 100px.
- Outer default stretch makes the inner item's final cross size definite.
- The green item's
max-height: 100%must then clamp against the appropriate resolved cross size. - The empty 9999px child intentionally supplies a tall intrinsic contribution. It is transparent, so excess green reveals the parent's height, not the child's paint.
flex-endexposes a height miscalculation by keeping the lower edge aligned and placing an oversized item above the intended square.
Web versus Lynx
In both cases, WebView paints a green square at physical x=[21,284), y=[354,617). Lynx paints additional green upward to content-viewport y=172, through the instruction paragraph. The intended square itself is pixel-identical; checking only that crop would falsely pass the case.
Each full comparison fails at 1.7410% against a 0.5% limit. The clipped screenshot does not establish the full offscreen green height, so this is not a claim that a 9999px green box was measured.
The adaptation retains the full outer → inner → green item → tall child hierarchy, both row layouts, all tested widths/maxima, default stretch, flex-end and transparent tall child. No tested node gains a definite height. Divs become views; the green item's default vertical Linear layout uses cross-axis stretch only to restore its child's auto width. Body/paragraph and content-box normalization do not replace the tested height chain. No omitted tested declaration or equivalent card-level repair was found.
Confirmed source-level refresh gap
The following is a confirmed conditional code path, not a traced call stack from the APK:
- When an existing layout algorithm is remeasured,
LayoutObject::UpdateMeasurecallsalgorithm_->Update(constraints)rather than initialization. LayoutAlgorithm::Updateinstalls the new container constraints, then resets algorithm bookkeeping:
void LayoutAlgorithm::Update(const Constraints& constraints) {
UpdateAvailableSizeAndMode(constraints);
Reset();
}Unlike initialization, this does not refresh child BoxInfo. A newly definite height is installed before the subsequent checks.
- For the row's unchanged definite width,
UpdateContainerMainSizereturns before its childUpdateBoxData()loop:
if (container_constraints_[kMainAxis].Mode() == SLMeasureModeDefinite &&
base::FloatsEqual(container_constraints_[kMainAxis].Size(),
container_main_size)) {
return;
}- Because the newly supplied height is already definite,
DetermineContainerCrossSizealso returns:
if (container_constraints_[kCrossAxis].Mode() == SLMeasureModeDefinite) {
return;
}This skips UpdateCrossSize and its child percentage-box refresh. Consequently a child can keep percentage-derived maxima from the preceding indefinite/older containing block even though the row is now definite.
Outer stretch supplies precisely this kind of definite remeasurement through DetermineUsedCrossSizeOfEachFlexItem. The green grandchild itself is flex-end, not stretch, so its non-stretch branch has no final corrective remeasurement. AlignItems uses actual margin-bound size:
offset += line_cross_size - cross_margin_bound;An oversized retained result therefore extends upward while its bottom remains correct, consistent with the observed pixels.
Additional risk and attribution limits
BoxInfo::UpdateBoxData can resolve the percentage maximum when called, but max-only changes do not set the dirty flag used to clear the measurement cache. This is a separate candidate hazard, not proof that the observed failure is a cache hit: UpdateMeasure applies minima/maxima to incoming constraints before lookup, so some changes already alter the cache key.
The missing refresh route above is statically established. Device-side intermediate maxima, cache hits and active compatibility flags have not been traced. Thus runtime attribution to that route is a strong investigation lead, not a claimed experimentally verified root cause; neither case establishes that all percentage maxima fail.
Expected correction and focused regressions
- Recompute percentage-dependent child BoxInfo whenever the effective containing-block size or definiteness changes, including a remeasurement that arrives already definite with an unchanged main-axis size.
- Ensure changed maximum constraints invalidate/recompute affected measured geometry and non-stretch hypothetical cross sizes before alignment.
- Add a nested row-flex AtMost/indefinite → definite cross-size regression with a max-only percentage constraint and a flex-end non-stretch grandchild.
- Cover both outer-max and inner-max WPT variants and supported compatibility modes. Keep the definite-height positive control
flexbox-definite-sizes-006passing. - Instrument old/new containing-block modes,
max_size_,UpdateBoxDatacalls and cache hits to confirm the deployed path before choosing a patch.
Do not hard-code a 100px item height, replace the percentage maximum, remove/shrink the tall child, change alignment or clip/crop the excess: these remove the WPT assertion.
Reproduce link
WPT sources above; adapted cards, evidence and full diagnosis are in lynx-wpt MR !192 (internal access required).
Reproduce Steps
- Render original
flexbox-definite-sizes-003.htmlin WebView and the correspondingtests/css-flexbox/flexbox-definite-sizes-003Lynx card at the same device scale. - Inspect the region above the expected green square, including the paragraph; do not compare only the square.
- Repeat for
flexbox-definite-sizes-004; preserve the distinct placement of the 100px maximum. - Instrument the transitions described above to distinguish skipped percentage refresh from a max-only cache/remeasurement issue on the affected binary.
Both isolated runs exited 1 with exact WebView-test-reference reports, independently audited PNGs and post-run cleanup. No WPT match-reference content was read. Each card retains evidence/{expected,actual,diff}.png and docs/evidence/css-flexbox-next-ten-20260915-<case>-summary.tsv; docs/diagnostics/flexbox-next-ten-20260915.md records the original pixel audit and limitations.
Duplicate check
Searched open/closed issues using the exact WPT names, max-height, percentage height, cross-size and flex-end. #8979 concerns premature definite sizing of wrapped items via aspect ratio, not failure to refresh descendants after a legitimate definite-size transition. #9014 is align-content value/reversal handling and #8980 baseline wrap-reverse offsets. No equivalent issue was found at filing time.
Source: lynx-family/lynx