Skip to content

Commit 8d4aeec

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
YGPersistentNodeCloningTest (#1813)
Summary: This adds a unit test to Yoga, which emulates the model of "persistent Yoga nodes" and cloning used by React Fabric, including the private (but relied on) Yoga APIs. It models the over-invalidation exposed in D75287261, which reproduces (due to Yoga incorrectly measuring flex-basis under fit-content, and that constraint changing when sibling changes) but this test for now sets a definite height on A, to show that we only clone what is neccesary, when measure constraints do not have to change. Having a minimal version of Fabric's model in Yoga unit tests should make some of these interesting interactions a bit easier to debug. Changelog: [Internal] Reviewed By: javache Differential Revision: D75572762
1 parent c935fd5 commit 8d4aeec

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
#include <yoga/config/Config.h>
11+
#include <yoga/node/Node.h>
12+
13+
#include <functional>
14+
#include <memory>
15+
#include <vector>
16+
17+
namespace facebook::yoga {
18+
19+
struct YGPersistentNodeCloningTest : public ::testing::Test {
20+
struct NodeWrapper {
21+
explicit NodeWrapper(
22+
YGConfigRef config,
23+
std::vector<std::shared_ptr<NodeWrapper>> children = {})
24+
: node{YGNodeNewWithConfig(config)}, children{std::move(children)} {
25+
YGNodeSetContext(node, this);
26+
27+
auto privateNode = resolveRef(node);
28+
for (const auto& child : this->children) {
29+
auto privateChild = resolveRef(child->node);
30+
// Claim first ownership of not yet owned nodes, to avoid immediately
31+
// cloning them
32+
if (YGNodeGetOwner(child->node) == nullptr) {
33+
privateChild->setOwner(privateNode);
34+
}
35+
privateNode->insertChild(privateChild, privateNode->getChildCount());
36+
}
37+
}
38+
39+
// Clone, with current children, for mutation
40+
NodeWrapper(const NodeWrapper& other)
41+
: node{YGNodeClone(other.node)}, children{other.children} {
42+
YGNodeSetContext(node, this);
43+
44+
auto privateNode = resolveRef(node);
45+
privateNode->setOwner(nullptr);
46+
}
47+
48+
// Clone, with new children
49+
NodeWrapper(
50+
const NodeWrapper& other,
51+
std::vector<std::shared_ptr<NodeWrapper>> children)
52+
: node{YGNodeClone(other.node)}, children{std::move(children)} {
53+
YGNodeSetContext(node, this);
54+
55+
auto privateNode = resolveRef(node);
56+
privateNode->setOwner(nullptr);
57+
privateNode->setChildren({});
58+
privateNode->setDirty(true);
59+
60+
for (const auto& child : this->children) {
61+
auto privateChild = resolveRef(child->node);
62+
// Claim first ownership of not yet owned nodes, to avoid immediately
63+
// cloning them
64+
if (YGNodeGetOwner(child->node) == nullptr) {
65+
privateChild->setOwner(privateNode);
66+
}
67+
privateNode->insertChild(privateChild, privateNode->getChildCount());
68+
}
69+
}
70+
71+
NodeWrapper(NodeWrapper&&) = delete;
72+
73+
~NodeWrapper() {
74+
YGNodeFree(node);
75+
}
76+
77+
NodeWrapper& operator=(const NodeWrapper& other) = delete;
78+
NodeWrapper& operator=(NodeWrapper&& other) = delete;
79+
80+
YGNodeRef node;
81+
std::vector<std::shared_ptr<NodeWrapper>> children;
82+
};
83+
84+
struct ConfigWrapper {
85+
ConfigWrapper() {
86+
YGConfigSetCloneNodeFunc(
87+
config,
88+
[](YGNodeConstRef oldNode, YGNodeConstRef owner, size_t childIndex) {
89+
onClone(oldNode, owner, childIndex);
90+
auto wrapper = static_cast<NodeWrapper*>(YGNodeGetContext(owner));
91+
auto old = static_cast<NodeWrapper*>(YGNodeGetContext(oldNode));
92+
93+
wrapper->children[childIndex] = std::make_shared<NodeWrapper>(*old);
94+
return wrapper->children[childIndex]->node;
95+
});
96+
}
97+
98+
ConfigWrapper(const ConfigWrapper&) = delete;
99+
ConfigWrapper(ConfigWrapper&&) = delete;
100+
101+
~ConfigWrapper() {
102+
YGConfigFree(config);
103+
}
104+
105+
ConfigWrapper& operator=(const ConfigWrapper&) = delete;
106+
ConfigWrapper& operator=(ConfigWrapper&&) = delete;
107+
108+
YGConfigRef config{YGConfigNew()};
109+
};
110+
111+
ConfigWrapper configWrapper;
112+
YGConfigRef config{configWrapper.config};
113+
114+
void SetUp() override {
115+
onClone = [](...) {};
116+
}
117+
118+
static inline std::function<void(YGNodeConstRef, YGNodeConstRef, size_t)>
119+
onClone;
120+
};
121+
122+
TEST_F(
123+
YGPersistentNodeCloningTest,
124+
changing_sibling_height_does_not_clone_neighbors) {
125+
// <ScrollView>
126+
// <View id="Sibling" style={{ height: 1 }} />
127+
// <View id="A" style={{ height: 1 }}>
128+
// <View id="B">
129+
// <View id="C">
130+
// <View id="D"/>
131+
// </View>
132+
// </View>
133+
// </View>
134+
// </ScrollView>
135+
136+
auto sibling = std::make_shared<NodeWrapper>(config);
137+
YGNodeStyleSetHeight(sibling->node, 1);
138+
139+
auto d = std::make_shared<NodeWrapper>(config);
140+
auto c = std::make_shared<NodeWrapper>(config, std::vector{d});
141+
auto b = std::make_shared<NodeWrapper>(config, std::vector{c});
142+
auto a = std::make_shared<NodeWrapper>(config, std::vector{b});
143+
YGNodeStyleSetHeight(a->node, 1);
144+
145+
auto scrollContentView =
146+
std::make_shared<NodeWrapper>(config, std::vector{sibling, a});
147+
YGNodeStyleSetPositionType(scrollContentView->node, YGPositionTypeAbsolute);
148+
149+
auto scrollView =
150+
std::make_shared<NodeWrapper>(config, std::vector{scrollContentView});
151+
YGNodeStyleSetWidth(scrollView->node, 100);
152+
YGNodeStyleSetHeight(scrollView->node, 100);
153+
154+
// We don't expect any cloning during the first layout
155+
onClone = [](...) { FAIL(); };
156+
157+
YGNodeCalculateLayout(
158+
scrollView->node, YGUndefined, YGUndefined, YGDirectionLTR);
159+
160+
auto siblingPrime = std::make_shared<NodeWrapper>(config);
161+
YGNodeStyleSetHeight(siblingPrime->node, 2);
162+
163+
auto scrollContentViewPrime = std::make_shared<NodeWrapper>(
164+
*scrollContentView, std::vector{siblingPrime, a});
165+
auto scrollViewPrime = std::make_shared<NodeWrapper>(
166+
*scrollView, std::vector{scrollContentViewPrime});
167+
168+
std::vector<NodeWrapper*> nodesCloned;
169+
// We should only need to clone "A"
170+
onClone = [&](YGNodeConstRef oldNode,
171+
YGNodeConstRef /*owner*/,
172+
size_t /*childIndex*/) {
173+
nodesCloned.push_back(static_cast<NodeWrapper*>(YGNodeGetContext(oldNode)));
174+
};
175+
176+
YGNodeCalculateLayout(
177+
scrollViewPrime->node, YGUndefined, YGUndefined, YGDirectionLTR);
178+
179+
EXPECT_EQ(nodesCloned.size(), 1);
180+
EXPECT_EQ(nodesCloned[0], a.get());
181+
}
182+
183+
} // namespace facebook::yoga

0 commit comments

Comments
 (0)