diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index b3febb84da..7012303234 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -3378,19 +3378,25 @@ class CSSRenderStyle extends RenderStyle maxConstraintWidth = childWrapper?.effectiveChildConstraints.maxWidth; } catch (_) {} - if (ancestorRenderStyle.isSelfRenderWidget() && + final RenderBoxModel? currentLayoutBoxForAncestor = + renderBoxModelInLayoutStack.isNotEmpty ? renderBoxModelInLayoutStack.last : null; + final bool ancestorIsAncestorInCurrentTree = currentLayoutBoxForAncestor == null + ? true + : isRenderSubtreeAncestor( + ancestorRenderStyle.attachedRenderBoxModel, + currentLayoutBoxForAncestor, + ); + + if (renderStyle.isSelfRenderWidget() && + !ancestorIsAncestorInCurrentTree && + childWrapper != null && + maxConstraintWidth != null && + maxConstraintWidth.isFinite) { + logicalWidth = maxConstraintWidth; + } else if (ancestorRenderStyle.isSelfRenderWidget() && childWrapper != null && maxConstraintWidth != null && maxConstraintWidth.isFinite) { - final RenderBoxModel? currentLayoutBoxForAncestor = - renderBoxModelInLayoutStack.isNotEmpty ? renderBoxModelInLayoutStack.last : null; - final bool ancestorIsAncestorInCurrentTree = currentLayoutBoxForAncestor == null - ? true - : isRenderSubtreeAncestor( - ancestorRenderStyle.attachedRenderBoxModel, - currentLayoutBoxForAncestor, - ); - if (ancestorIsAncestorInCurrentTree) { final double? ancestorContentLogicalWidth = ancestorRenderStyle.contentBoxLogicalWidth; if (ancestorContentLogicalWidth != null && ancestorContentLogicalWidth.isFinite) { diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 3f0c55aecf..8511cd9b24 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -120,7 +120,8 @@ bool debugRenderObjectNeedsLayout(RenderObject renderObject) { return result; } -bool canReuseStableProxyChildLayout(RenderBox? child, BoxConstraints constraints) { +bool canReuseStableProxyChildLayout( + RenderBox? child, BoxConstraints constraints) { if (child == null || !child.hasSize || child.constraints != constraints) { return false; } @@ -434,11 +435,13 @@ abstract class RenderBoxModel extends RenderBox // Whether it needs relayout due to percentage calculation. bool needsRelayout = false; + bool _hasPendingLayoutUpdate = true; bool _hasPendingIntrinsicMeasurementInvalidation = true; bool _hasPendingSubtreeIntrinsicMeasurementInvalidation = true; String? _debugIntrinsicMeasurementDirtyReason = 'initial'; int _clearIntrinsicMeasurementInvalidationAfterLayoutPass = 1; + bool get hasPendingLayoutUpdate => _hasPendingLayoutUpdate; bool get hasPendingIntrinsicMeasurementInvalidation => _hasPendingIntrinsicMeasurementInvalidation; bool get hasPendingSubtreeIntrinsicMeasurementInvalidation => @@ -462,6 +465,11 @@ abstract class RenderBoxModel extends RenderBox needsRelayout = true; } + @protected + void clearPendingLayoutUpdateForCurrentLayoutPass() { + _hasPendingLayoutUpdate = false; + } + void markNeedsIntrinsicMeasurementUpdate([String reason = 'unspecified']) { _hasPendingIntrinsicMeasurementInvalidation = true; _debugIntrinsicMeasurementDirtyReason = reason; @@ -525,8 +533,10 @@ abstract class RenderBoxModel extends RenderBox @override void layout(Constraints constraints, {bool parentUsesSize = false}) { - _lastLaidOutAsRelayoutBoundary = - !parentUsesSize || sizedByParent || constraints.isTight || parent == null; + _lastLaidOutAsRelayoutBoundary = !parentUsesSize || + sizedByParent || + constraints.isTight || + parent == null; if (renderBoxModelInLayoutStack.isEmpty) { renderBoxModelLayoutPassId++; } @@ -649,9 +659,8 @@ abstract class RenderBoxModel extends RenderBox // width of 0 (e.g., auto-width flex item whose only child is out-of-flow). // In such cases, the containing block still exists, but the abspos box should size // from its own content rather than the flex item's zero content width. - final bool isAbsOrFixed = - style.position == CSSPositionType.absolute || - style.position == CSSPositionType.fixed; + final bool isAbsOrFixed = style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed; if (isAbsOrFixed && style.width.isAuto && style.isParentRenderFlexLayout() && @@ -681,8 +690,7 @@ abstract class RenderBoxModel extends RenderBox } } else { // Not in a flex context, inherit parent's content width constraint normally - double parentContentWidth = - parentFlowStyle.contentMaxConstraintsWidth; + double parentContentWidth = parentFlowStyle.contentMaxConstraintsWidth; if (parentContentWidth != double.infinity) { parentBoxContentConstraintsWidth = parentContentWidth; } @@ -710,7 +718,7 @@ abstract class RenderBoxModel extends RenderBox !style.isSelfRenderReplaced() && style.borderBoxLogicalWidth == null && parentBoxContentConstraintsWidth != null) { - parentBoxContentConstraintsWidth = null; + parentBoxContentConstraintsWidth = null; } double? containingBlockPaddingBoxWidth; @@ -731,12 +739,10 @@ abstract class RenderBoxModel extends RenderBox style.minWidth.isAuto ? null : style.minWidth.computedValue; double? maxWidth = style.maxWidth.isNone ? null : style.maxWidth.computedValue; - double? minHeight = style.minHeight.isAuto - ? null - : style.minHeight.computedValue; - double? maxHeight = style.maxHeight.isNone - ? null - : style.maxHeight.computedValue; + double? minHeight = + style.minHeight.isAuto ? null : style.minHeight.computedValue; + double? maxHeight = + style.maxHeight.isNone ? null : style.maxHeight.computedValue; double maxConstraintWidth = style.borderBoxLogicalWidth ?? parentBoxContentConstraintsWidth ?? @@ -747,9 +753,8 @@ abstract class RenderBoxModel extends RenderBox // This handles cases where style-tree logical widths are unavailable (e.g., parent inline-block // with auto width) but the containing block has been measured. See: // https://www.w3.org/TR/css-position-3/#abs-non-replaced-width - final bool isAbsOrFixed = - style.position == CSSPositionType.absolute || - style.position == CSSPositionType.fixed; + final bool isAbsOrFixed = style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed; if (maxConstraintWidth == double.infinity && isAbsOrFixed && !style.isSelfRenderReplaced() && @@ -832,8 +837,7 @@ abstract class RenderBoxModel extends RenderBox } // Height should be not smaller than border and padding in vertical direction // when box-sizing is border-box which is only supported. - double minConstraintHeight = - styleBorder.vertical + stylePadding.vertical; + double minConstraintHeight = styleBorder.vertical + stylePadding.vertical; double maxConstraintHeight = style.borderBoxLogicalHeight ?? double.infinity; @@ -876,8 +880,7 @@ abstract class RenderBoxModel extends RenderBox maxConstraintWidth = maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; // Only reduce minConstraintWidth if maxWidth is larger than border+padding requirements - double borderPadding = - styleBorder.horizontal + stylePadding.horizontal; + double borderPadding = styleBorder.horizontal + stylePadding.horizontal; if (maxWidth >= borderPadding) { minConstraintWidth = minConstraintWidth > maxWidth ? maxWidth : minConstraintWidth; @@ -968,6 +971,7 @@ abstract class RenderBoxModel extends RenderBox @override void markNeedsLayout() { + _hasPendingLayoutUpdate = true; final RenderObject? relayoutParent = _relayoutParentOnSizeChange; // Some wrapper parents mirror child.boxSize while laying the child out // with parentUsesSize: false to keep a local relayout boundary. @@ -1111,6 +1115,7 @@ abstract class RenderBoxModel extends RenderBox this.contentConstraints = contentConstraints; clearOverflowLayout(); isSelfSizeChanged = false; + clearPendingLayoutUpdateForCurrentLayoutPass(); updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); // Reset cached CSS baselines before a new layout pass. They will be diff --git a/webf/lib/src/rendering/box_wrapper.dart b/webf/lib/src/rendering/box_wrapper.dart index 2ac95ed2a2..de59100028 100644 --- a/webf/lib/src/rendering/box_wrapper.dart +++ b/webf/lib/src/rendering/box_wrapper.dart @@ -97,6 +97,7 @@ class RenderLayoutBoxWrapper extends RenderBoxModel @override void performLayout() { + clearPendingLayoutUpdateForCurrentLayoutPass(); renderStyle.computeContentBoxLogicalWidth(); renderStyle.computeContentBoxLogicalHeight(); @@ -106,7 +107,8 @@ class RenderLayoutBoxWrapper extends RenderBoxModel final RenderBox? c = child; if (c == null) { size = constraints.constrain(Size.zero); - initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); + initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), + Rect.fromLTRB(0, 0, size.width, size.height)); _lastWrapperConstraints = constraints; _lastResolvedChildConstraints = null; return; @@ -127,9 +129,13 @@ class RenderLayoutBoxWrapper extends RenderBoxModel // CSS logic already accounts for its padding/border. } else if (c is RenderTextBox) { // Text nodes inside wrappers should measure themselves with a sensible bound. - final double maxW = constraints.hasBoundedWidth ? constraints.maxWidth : double.infinity; - final double maxH = constraints.hasBoundedHeight ? constraints.maxHeight : double.infinity; - childConstraints = BoxConstraints(minWidth: 0, maxWidth: maxW, minHeight: 0, maxHeight: maxH); + final double maxW = + constraints.hasBoundedWidth ? constraints.maxWidth : double.infinity; + final double maxH = constraints.hasBoundedHeight + ? constraints.maxHeight + : double.infinity; + childConstraints = BoxConstraints( + minWidth: 0, maxWidth: maxW, minHeight: 0, maxHeight: maxH); } else { // Fallback: provide loose, unbounded constraints so inner WebF render boxes // can compute their own CSS-based constraints without being forced to expand. @@ -158,7 +164,8 @@ class RenderLayoutBoxWrapper extends RenderBoxModel } if (minW > maxW) minW = maxW; if (minH > maxH) minH = maxH; - return BoxConstraints(minWidth: minW, maxWidth: maxW, minHeight: minH, maxHeight: maxH); + return BoxConstraints( + minWidth: minW, maxWidth: maxW, minHeight: minH, maxHeight: maxH); } childConstraints = intersect(childConstraints, constraints); @@ -176,7 +183,8 @@ class RenderLayoutBoxWrapper extends RenderBoxModel // Use sibling-oriented collapsed margins so the inter-item spacing equals // the CSS collapsed result between previous bottom and current top. final double childMarginTop = renderStyle.collapsedMarginTopForSibling; - final double childMarginBottom = renderStyle.collapsedMarginBottomForSibling; + final double childMarginBottom = + renderStyle.collapsedMarginBottomForSibling; final double childMarginLeft = renderStyle.marginLeft.computedValue; final double childMarginRight = renderStyle.marginRight.computedValue; @@ -186,34 +194,47 @@ class RenderLayoutBoxWrapper extends RenderBoxModel // elements where content can extend beyond the box; for clipping containers // the intermediate RenderLayoutBoxWrapper (created for the Scrollable chain) // never propagates scrollableSize, leaving it at Size.zero. - final bool isClippingContainer = c.renderStyle.effectiveOverflowX != CSSOverflowType.visible || - c.renderStyle.effectiveOverflowY != CSSOverflowType.visible; - final Size contentScrollable = isClippingContainer ? c.size : c.scrollableSize; + final bool isClippingContainer = + c.renderStyle.effectiveOverflowX != CSSOverflowType.visible || + c.renderStyle.effectiveOverflowY != CSSOverflowType.visible; + final Size contentScrollable = + isClippingContainer ? c.size : c.scrollableSize; // Decide sizing based on list axis. In a horizontal list (unbounded width), // widen by left+right margins so gaps appear between items. In a vertical // list (unbounded height), increase height by top+bottom margins. - final bool isHorizontalList = constraints.hasBoundedHeight && !constraints.hasBoundedWidth; - final bool isVerticalList = constraints.hasBoundedWidth && !constraints.hasBoundedHeight; + final bool isHorizontalList = + constraints.hasBoundedHeight && !constraints.hasBoundedWidth; + final bool isVerticalList = + constraints.hasBoundedWidth && !constraints.hasBoundedHeight; double wrapperWidth; double wrapperHeight; if (isHorizontalList) { - wrapperWidth = contentScrollable.width + childMarginLeft + childMarginRight; + wrapperWidth = + contentScrollable.width + childMarginLeft + childMarginRight; // Height is tight from the viewport; still offset child by vertical margins below. - wrapperHeight = constraints.hasBoundedHeight ? constraints.maxHeight : (contentScrollable.height + childMarginTop + childMarginBottom); + wrapperHeight = constraints.hasBoundedHeight + ? constraints.maxHeight + : (contentScrollable.height + childMarginTop + childMarginBottom); } else if (isVerticalList) { - wrapperWidth = constraints.hasBoundedWidth ? constraints.maxWidth : (contentScrollable.width + childMarginLeft + childMarginRight); - wrapperHeight = contentScrollable.height + childMarginTop + childMarginBottom; + wrapperWidth = constraints.hasBoundedWidth + ? constraints.maxWidth + : (contentScrollable.width + childMarginLeft + childMarginRight); + wrapperHeight = + contentScrollable.height + childMarginTop + childMarginBottom; } else { // Fallback (no tightness info): include both margins conservatively. - wrapperWidth = contentScrollable.width + childMarginLeft + childMarginRight; - wrapperHeight = contentScrollable.height + childMarginTop + childMarginBottom; + wrapperWidth = + contentScrollable.width + childMarginLeft + childMarginRight; + wrapperHeight = + contentScrollable.height + childMarginTop + childMarginBottom; } size = constraints.constrain(Size(wrapperWidth, wrapperHeight)); - if (renderStyle.isSelfPositioned() || renderStyle.isSelfStickyPosition()) { + if (renderStyle.isSelfPositioned() || + renderStyle.isSelfStickyPosition()) { CSSPositionedLayout.applyPositionedChildOffset(this, c); } else { // Offset the child within the wrapper by its margins @@ -226,7 +247,8 @@ class RenderLayoutBoxWrapper extends RenderBoxModel } calculateBaseline(); - initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); + initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), + Rect.fromLTRB(0, 0, size.width, size.height)); } @override @@ -241,7 +263,9 @@ class RenderLayoutBoxWrapper extends RenderBoxModel @override bool hitTest(BoxHitTestResult result, {required Offset position}) { - if (!hasSize || !contentVisibilityHitTest(result, position: position) || renderStyle.isVisibilityHidden) { + if (!hasSize || + !contentVisibilityHitTest(result, position: position) || + renderStyle.isVisibilityHidden) { return false; } @@ -249,9 +273,12 @@ class RenderLayoutBoxWrapper extends RenderBoxModel if (!hasSize) { if (debugNeedsLayout) { throw FlutterError.fromParts([ - ErrorSummary('Cannot hit test a render box that has never been laid out.'), - describeForError('The hitTest() method was called on this RenderBox'), - ErrorDescription("Unfortunately, this object's geometry is not known at this time, " + ErrorSummary( + 'Cannot hit test a render box that has never been laid out.'), + describeForError( + 'The hitTest() method was called on this RenderBox'), + ErrorDescription( + "Unfortunately, this object's geometry is not known at this time, " 'probably because it has never been laid out. ' 'This means it cannot be accurately hit-tested.'), ErrorHint('If you are trying ' @@ -263,7 +290,8 @@ class RenderLayoutBoxWrapper extends RenderBoxModel throw FlutterError.fromParts([ ErrorSummary('Cannot hit test a render box with no size.'), describeForError('The hitTest() method was called on this RenderBox'), - ErrorDescription('Although this node is not marked as needing layout, ' + ErrorDescription( + 'Although this node is not marked as needing layout, ' 'its size is not set.'), ErrorHint('A RenderBox object must have an ' 'explicit size before it can be hit-tested. Make sure ' @@ -283,14 +311,18 @@ class RenderLayoutBoxWrapper extends RenderBoxModel } final BoxParentData childParentData = child!.parentData as BoxParentData; - bool isHit = result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: (result, position) { - // addWithPaintOffset is to add an offset to the child node, the calculation itself does not need to bring an offset. - if (hasSize && hitTestChildren(result, position: position) || hitTestSelf(position)) { - result.add(BoxHitTestEntry(this, position)); - return true; - } - return false; - }); + bool isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (result, position) { + // addWithPaintOffset is to add an offset to the child node, the calculation itself does not need to bring an offset. + if (hasSize && hitTestChildren(result, position: position) || + hitTestSelf(position)) { + result.add(BoxHitTestEntry(this, position)); + return true; + } + return false; + }); return isHit; } @@ -304,7 +336,9 @@ class RenderLayoutBoxWrapper extends RenderBoxModel class LayoutBoxWrapper extends SingleChildRenderObjectWidget { final dom.Element ownerElement; - const LayoutBoxWrapper({super.key, required Widget child, required this.ownerElement}) : super(child: child); + const LayoutBoxWrapper( + {super.key, required Widget child, required this.ownerElement}) + : super(child: child); @override RenderObject createRenderObject(BuildContext context) { diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 5e4f0890d4..33943698a7 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -175,6 +175,7 @@ class RenderEventListener extends RenderBoxModel @override void performLayout() { + clearPendingLayoutUpdateForCurrentLayoutPass(); updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); final RenderBox? currentChild = child; if (hasSize && diff --git a/webf/lib/src/rendering/inline_items_builder.dart b/webf/lib/src/rendering/inline_items_builder.dart index d5e1bd36ab..37e1ec6dc3 100644 --- a/webf/lib/src/rendering/inline_items_builder.dart +++ b/webf/lib/src/rendering/inline_items_builder.dart @@ -446,10 +446,16 @@ class InlineItemsBuilder { if (_boxStack.isEmpty) { _consumedRootContent = true; } - // Allow inline-block/inline-flex, or inline replaced/widget elements. + // Allow: + // - inline-block / inline-flex + // - inline replaced / inline widget elements + // - block / flex children that are intentionally represented as atomic + // placeholders when they split an inline formatting context assert( box.renderStyle.display == CSSDisplay.inlineBlock || box.renderStyle.display == CSSDisplay.inlineFlex || + box.renderStyle.display == CSSDisplay.block || + box.renderStyle.display == CSSDisplay.flex || (box.renderStyle.display == CSSDisplay.inline && (box.renderStyle.isSelfRenderReplaced() || box.renderStyle.isSelfRenderWidget())) ); diff --git a/webf/lib/src/rendering/layout_box.dart b/webf/lib/src/rendering/layout_box.dart index 1cc0d108a0..6132102abc 100644 --- a/webf/lib/src/rendering/layout_box.dart +++ b/webf/lib/src/rendering/layout_box.dart @@ -153,12 +153,22 @@ abstract class RenderLayoutBox extends RenderBoxModel final RenderLayoutParentData parentData = child.parentData as RenderLayoutParentData; parentData.semanticsIndex = index; - visitor(child); + if (!_shouldSkipSemanticsProxyChild(child)) { + visitor(child); + } child = parentData.nextSibling; index++; } } + bool _shouldSkipSemanticsProxyChild(RenderBox child) { + if (child is! RenderBoxModel || + (child is! RenderEventListener && child is! RenderLayoutBoxWrapper)) { + return false; + } + return child.hasPendingLayoutUpdate || child.needsRelayout; + } + // Aggregate intrinsic sizing over non-positioned children. @override double computeMinIntrinsicWidth(double height) { diff --git a/webf/test/src/html/accessibility_semantics_dirty_wrapper_test.dart b/webf/test/src/html/accessibility_semantics_dirty_wrapper_test.dart new file mode 100644 index 0000000000..07cc99147f --- /dev/null +++ b/webf/test/src/html/accessibility_semantics_dirty_wrapper_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf/dom.dart' as dom; +import 'package:webf/rendering.dart'; +import 'package:webf/src/rendering/event_listener.dart'; +import 'package:webf/src/rendering/layout_box.dart'; + +import '../../setup.dart'; +import '../widget/test_utils.dart'; + +void main() { + setUp(() { + setupTest(); + }); + + testWidgets('semantics traversal should not visit a dirty transparent event-listener wrapper', ( + WidgetTester tester, + ) async { + final html = ''' +
+
+ Order 1 +
+
+ '''; + + final prepared = await WebFWidgetTestUtils.prepareWidgetTest( + tester: tester, + html: '$html', + controllerName: 'a11y-dirty-wrapper-${DateTime.now().millisecondsSinceEpoch}', + wrap: (child) => MaterialApp(home: Scaffold(body: child)), + ); + + final dom.Element container = prepared.getElementById('container'); + final dom.Element row = prepared.getElementById('row'); + + final RenderLayoutBox layoutBox = + container.renderStyle.attachedRenderBoxModel! as RenderLayoutBox; + final RenderEventListener eventListener = row.attachedRendererEventListener!; + + eventListener.markNeedsLayout(); + + final List visited = []; + layoutBox.visitChildrenForSemantics(visited.add); + + expect(eventListener.debugNeedsLayout, isTrue); + expect( + visited.any((RenderObject child) => identical(child, eventListener)), + isFalse, + reason: 'Semantics traversal should unwrap or skip transparent dirty wrappers.', + ); + }); +} diff --git a/webf/test/src/rendering/inline_atomic_block_assertion_test.dart b/webf/test/src/rendering/inline_atomic_block_assertion_test.dart new file mode 100644 index 0000000000..c867c6aca7 --- /dev/null +++ b/webf/test/src/rendering/inline_atomic_block_assertion_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf/webf.dart'; + +import '../../setup.dart'; +import '../widget/test_utils.dart'; + +void main() { + setUpAll(() { + setupTest(); + }); + + setUp(() { + WebFControllerManager.instance.initialize( + WebFControllerManagerConfig( + maxAliveInstances: 5, + maxAttachedInstances: 5, + enableDevTools: false, + ), + ); + }); + + tearDown(() async { + WebFControllerManager.instance.disposeAll(); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + testWidgets('block child inside inline content should not trip atomic inline assertion', (WidgetTester tester) async { + await WebFWidgetTestUtils.prepareWidgetTest( + tester: tester, + controllerName: 'inline-atomic-block-${DateTime.now().millisecondsSinceEpoch}', + html: ''' +
+ + before +
block child
+ after +
+
+ ''', + ); + + await tester.pump(); + expect(tester.takeException(), isNull); + }); +} diff --git a/webf/test/src/rendering/inline_span_local_ifc_under_widget_test.dart b/webf/test/src/rendering/inline_span_local_ifc_under_widget_test.dart index b850b80f89..fbef43b183 100644 --- a/webf/test/src/rendering/inline_span_local_ifc_under_widget_test.dart +++ b/webf/test/src/rendering/inline_span_local_ifc_under_widget_test.dart @@ -87,4 +87,25 @@ void main() { expect(ifc!.paragraphLineMetrics.length, 1); expect(ifc.paragraphLineMetrics.first.height, moreOrLessEquals(24.0, epsilon: 0.8)); }); + + testWidgets('inline under RenderWidget with flex child should not trip atomic inline assertion', (WidgetTester tester) async { + await WebFWidgetTestUtils.prepareWidgetTest( + tester: tester, + controllerName: 'inline-span-flex-child-${DateTime.now().millisecondsSinceEpoch}', + html: ''' + + + before +
+ nested +
+ after +
+
+ ''', + ); + + await tester.pump(); + expect(tester.takeException(), isNull); + }); }