|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +#include <gtest/gtest.h> |
| 9 | +#include <yoga/Yoga.h> |
| 10 | + |
| 11 | +namespace facebook::yoga { |
| 12 | + |
| 13 | +// Regression test for `cleanupContentsNodesRecursively` stamping |
| 14 | +// `hasNewLayout=true` on a display:contents child during a measurement-only |
| 15 | +// visit. |
| 16 | +// |
| 17 | +// Setup: Root (overflow=visible, flex column) -> Parent (flex-grow=1) |
| 18 | +// -> Contents (display:contents) -> Leaf. |
| 19 | +// Flipping root's overflow between the two passes invalidates Parent's |
| 20 | +// measurement cache (computeFlexBasisForChild's `applyHeightFitContent` |
| 21 | +// branch flips) but leaves Parent's layout cache intact (its allotment is |
| 22 | +// unchanged). So in pass 2, Parent's calculateLayoutImpl runs only with |
| 23 | +// performLayout=false - the layout-phase visit is served from cache and |
| 24 | +// `cleanupContentsNodesRecursively` never runs at performLayout=true. |
| 25 | +TEST(YogaTest, contents_child_hasNewLayout_not_stamped_on_measure_only_visit) { |
| 26 | + YGNodeRef leaf = YGNodeNew(); |
| 27 | + YGNodeStyleSetWidth(leaf, 20); |
| 28 | + YGNodeStyleSetHeight(leaf, 20); |
| 29 | + |
| 30 | + YGNodeRef contents = YGNodeNew(); |
| 31 | + YGNodeStyleSetDisplay(contents, YGDisplayContents); |
| 32 | + YGNodeInsertChild(contents, leaf, 0); |
| 33 | + |
| 34 | + YGNodeRef parent = YGNodeNew(); |
| 35 | + YGNodeStyleSetFlexGrow(parent, 1); |
| 36 | + YGNodeInsertChild(parent, contents, 0); |
| 37 | + |
| 38 | + YGNodeRef root = YGNodeNew(); |
| 39 | + YGNodeStyleSetFlexDirection(root, YGFlexDirectionColumn); |
| 40 | + YGNodeStyleSetWidth(root, 200); |
| 41 | + YGNodeStyleSetHeight(root, 200); |
| 42 | + YGNodeStyleSetOverflow(root, YGOverflowVisible); |
| 43 | + YGNodeInsertChild(root, parent, 0); |
| 44 | + |
| 45 | + YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR); |
| 46 | + |
| 47 | + // Simulate a consumer (e.g. React Native's layout pass) reading and |
| 48 | + // clearing the hasNewLayout flags. |
| 49 | + YGNodeSetHasNewLayout(root, false); |
| 50 | + YGNodeSetHasNewLayout(parent, false); |
| 51 | + YGNodeSetHasNewLayout(contents, false); |
| 52 | + YGNodeSetHasNewLayout(leaf, false); |
| 53 | + |
| 54 | + YGNodeStyleSetOverflow(root, YGOverflowScroll); |
| 55 | + YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR); |
| 56 | + |
| 57 | + EXPECT_FALSE(YGNodeGetHasNewLayout(contents)) |
| 58 | + << "contents.hasNewLayout was stamped during a measure-only visit " |
| 59 | + "(cleanupContentsNodesRecursively ran with performLayout=false but " |
| 60 | + "no matching performLayout=true visit occurred this pass)"; |
| 61 | + |
| 62 | + YGNodeFreeRecursive(root); |
| 63 | +} |
| 64 | + |
| 65 | +// Regression test for `cleanupContentsNodesRecursively` invoked from |
| 66 | +// `layoutAbsoluteDescendants`: it must stamp `hasNewLayout=true` on |
| 67 | +// display:contents children on the path to an absolute descendant whose |
| 68 | +// position changed this pass. Otherwise consumers traversing the tree via |
| 69 | +// hasNewLayout would skip the contents subtree and miss the update. |
| 70 | +// |
| 71 | +// Setup: root (containing block) -> staticChild (fixed 50x50) |
| 72 | +// -> contents (display:contents) -> absoluteChild (right/bottom- |
| 73 | +// anchored so its position depends on the containing block). |
| 74 | +// Growing root in pass 2 dirties only root. staticChild's fixed dimensions |
| 75 | +// make its layout cache hit, so its main-path cleanup never runs. |
| 76 | +// absoluteChild depends on the containing block and is repositioned by |
| 77 | +// `layoutAbsoluteDescendants`, which is the only path that can stamp |
| 78 | +// contents along the way. |
| 79 | +TEST( |
| 80 | + YogaTest, |
| 81 | + absolute_descendant_through_contents_is_reachable_via_hasNewLayout) { |
| 82 | + YGNodeRef absoluteChild = YGNodeNew(); |
| 83 | + YGNodeStyleSetPositionType(absoluteChild, YGPositionTypeAbsolute); |
| 84 | + YGNodeStyleSetPosition(absoluteChild, YGEdgeRight, 0); |
| 85 | + YGNodeStyleSetPosition(absoluteChild, YGEdgeBottom, 0); |
| 86 | + YGNodeStyleSetWidth(absoluteChild, 10); |
| 87 | + YGNodeStyleSetHeight(absoluteChild, 10); |
| 88 | + |
| 89 | + YGNodeRef contents = YGNodeNew(); |
| 90 | + YGNodeStyleSetDisplay(contents, YGDisplayContents); |
| 91 | + YGNodeInsertChild(contents, absoluteChild, 0); |
| 92 | + |
| 93 | + YGNodeRef staticChild = YGNodeNew(); |
| 94 | + YGNodeStyleSetPositionType(staticChild, YGPositionTypeStatic); |
| 95 | + YGNodeStyleSetWidth(staticChild, 50); |
| 96 | + YGNodeStyleSetHeight(staticChild, 50); |
| 97 | + YGNodeInsertChild(staticChild, contents, 0); |
| 98 | + |
| 99 | + YGNodeRef root = YGNodeNew(); |
| 100 | + YGNodeStyleSetWidth(root, 100); |
| 101 | + YGNodeStyleSetHeight(root, 100); |
| 102 | + YGNodeInsertChild(root, staticChild, 0); |
| 103 | + |
| 104 | + YGNodeCalculateLayout(root, 100, 100, YGDirectionLTR); |
| 105 | + |
| 106 | + // Simulate a consumer (e.g. React Native's layout pass) reading and |
| 107 | + // clearing the hasNewLayout flags. |
| 108 | + YGNodeSetHasNewLayout(root, false); |
| 109 | + YGNodeSetHasNewLayout(staticChild, false); |
| 110 | + YGNodeSetHasNewLayout(contents, false); |
| 111 | + YGNodeSetHasNewLayout(absoluteChild, false); |
| 112 | + |
| 113 | + YGNodeStyleSetWidth(root, 150); |
| 114 | + YGNodeCalculateLayout(root, 150, 100, YGDirectionLTR); |
| 115 | + |
| 116 | + ASSERT_TRUE(YGNodeGetHasNewLayout(absoluteChild)); |
| 117 | + EXPECT_TRUE(YGNodeGetHasNewLayout(staticChild)); |
| 118 | + EXPECT_TRUE(YGNodeGetHasNewLayout(contents)) |
| 119 | + << "contents node on the path to a freshly-positioned absolute " |
| 120 | + "descendant must have hasNewLayout=true so consumers can traverse " |
| 121 | + "to it"; |
| 122 | + |
| 123 | + YGNodeFreeRecursive(root); |
| 124 | +} |
| 125 | + |
| 126 | +// Regression test for `cleanupContentsNodesRecursively` invoked from |
| 127 | +// `layoutAbsoluteDescendants`: it must not stamp `hasNewLayout=true` on |
| 128 | +// display:contents children when no new layout was produced for their |
| 129 | +// parent this pass. Otherwise the stale flag survives across passes and |
| 130 | +// can be observed by a later cache-hit on the parent. |
| 131 | +// |
| 132 | +// Setup: root -> a (fixed 50x50) -> b (fixed 30x30) |
| 133 | +// -> contents (display:contents) -> leaf. |
| 134 | +// Flipping root's overflow in pass 2 dirties only root. a and b have fixed |
| 135 | +// sizes so their layout caches hit; a.calculateLayoutImpl is skipped, so |
| 136 | +// b.calculateLayoutInternal is never invoked. `layoutAbsoluteDescendants` |
| 137 | +// still walks down through a and b looking for absolute descendants, but |
| 138 | +// there are none beneath b - so b.hasNewLayout stays false and the |
| 139 | +// cleanup along that walk must leave contents unflagged. |
| 140 | +TEST( |
| 141 | + YogaTest, |
| 142 | + absolute_phase_cleanup_does_not_stamp_when_parent_layout_skipped) { |
| 143 | + YGNodeRef leaf = YGNodeNew(); |
| 144 | + YGNodeStyleSetWidth(leaf, 10); |
| 145 | + YGNodeStyleSetHeight(leaf, 10); |
| 146 | + |
| 147 | + YGNodeRef contents = YGNodeNew(); |
| 148 | + YGNodeStyleSetDisplay(contents, YGDisplayContents); |
| 149 | + YGNodeInsertChild(contents, leaf, 0); |
| 150 | + |
| 151 | + YGNodeRef b = YGNodeNew(); |
| 152 | + YGNodeStyleSetPositionType(b, YGPositionTypeStatic); |
| 153 | + YGNodeStyleSetWidth(b, 30); |
| 154 | + YGNodeStyleSetHeight(b, 30); |
| 155 | + YGNodeInsertChild(b, contents, 0); |
| 156 | + |
| 157 | + YGNodeRef a = YGNodeNew(); |
| 158 | + YGNodeStyleSetPositionType(a, YGPositionTypeStatic); |
| 159 | + YGNodeStyleSetWidth(a, 50); |
| 160 | + YGNodeStyleSetHeight(a, 50); |
| 161 | + YGNodeInsertChild(a, b, 0); |
| 162 | + |
| 163 | + YGNodeRef root = YGNodeNew(); |
| 164 | + YGNodeStyleSetWidth(root, 200); |
| 165 | + YGNodeStyleSetHeight(root, 200); |
| 166 | + YGNodeStyleSetOverflow(root, YGOverflowVisible); |
| 167 | + YGNodeInsertChild(root, a, 0); |
| 168 | + |
| 169 | + YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR); |
| 170 | + |
| 171 | + // Simulate a consumer (e.g. React Native's layout pass) reading and |
| 172 | + // clearing the hasNewLayout flags. |
| 173 | + YGNodeSetHasNewLayout(root, false); |
| 174 | + YGNodeSetHasNewLayout(a, false); |
| 175 | + YGNodeSetHasNewLayout(b, false); |
| 176 | + YGNodeSetHasNewLayout(contents, false); |
| 177 | + YGNodeSetHasNewLayout(leaf, false); |
| 178 | + |
| 179 | + YGNodeStyleSetOverflow(root, YGOverflowScroll); |
| 180 | + YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR); |
| 181 | + |
| 182 | + EXPECT_FALSE(YGNodeGetHasNewLayout(b)); |
| 183 | + EXPECT_FALSE(YGNodeGetHasNewLayout(contents)) |
| 184 | + << "contents.hasNewLayout was stamped during a walk where its " |
| 185 | + "parent's hasNewLayout remained false this pass"; |
| 186 | + |
| 187 | + YGNodeFreeRecursive(root); |
| 188 | +} |
| 189 | + |
| 190 | +} // namespace facebook::yoga |
0 commit comments