Skip to content

Commit b017ecb

Browse files
javachemeta-codesync[bot]
authored andcommitted
Add regression tests for YogaLayoutableShadowNode clone paths (#57018)
Summary: Pull Request resolved: #57018 Lock in the behaviour of the two branches in `YogaLayoutableShadowNode`'s clone constructor: cloning with `!fragment.children` (children inherited from the source) and cloning with `fragment.children` set (children list replaced). The fixture also covers successive prop-only clones (the animation commit pattern), verifies that mutating a clone does not bleed back into the source's layout state, asserts that the fast path shares the source's yoga-child pointers without disturbing the source's ownership, and pins down `cloneTree`'s "path + siblings get cloned, deeper descendants stay shared" invariant. To let the fixture inspect `YogaLayoutableShadowNode::yogaNode_`, `YogaCloneTest` is declared as a friend in the header. The class is only defined in the tests target, so production code sees the friend as a forward declaration with no effect. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D107076027 fbshipit-source-id: 7114d6b4abc58c6c0ca69c633b8d888cfdd84dbe
1 parent ed893f7 commit b017ecb

2 files changed

Lines changed: 353 additions & 0 deletions

File tree

packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
namespace facebook::react {
2323

2424
class YogaLayoutableShadowNode : public LayoutableShadowNode {
25+
// Allow YogaCloneTest to read yogaNode_ for ownership assertions in unit
26+
// tests. The class is only defined in the tests target; production code
27+
// sees the friend as a forward declaration with no effect.
28+
friend class YogaCloneTest;
29+
2530
public:
2631
using Shared = std::shared_ptr<const YogaLayoutableShadowNode>;
2732
using ListOfShared = std::vector<Shared>;

packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
#include <react/renderer/core/PropsParserContext.h>
1818
#include <react/renderer/element/ComponentBuilder.h>
1919

20+
#include <react/renderer/components/view/YogaLayoutableShadowNode.h>
2021
#include <react/renderer/element/Element.h>
2122
#include <react/renderer/element/testUtils.h>
23+
#include <yoga/node/Node.h>
2224
#include <yoga/numeric/FloatOptional.h>
2325

2426
namespace facebook::react {
@@ -263,4 +265,350 @@ TEST_F(YogaDirtyFlagTest, clonedPropsPreserveAspectRatio) {
263265
});
264266
}
265267

268+
// Exercises the two branches of YogaLayoutableShadowNode's clone constructor:
269+
// `!fragment.children` (children inherited from source) and
270+
// `fragment.children` set (children list replaced). The cloned subtree must
271+
// lay out identically to a freshly built equivalent tree.
272+
class YogaCloneTest : public ::testing::Test {
273+
protected:
274+
ComponentBuilder builder_;
275+
std::shared_ptr<RootShadowNode> rootShadowNode_;
276+
std::shared_ptr<ViewShadowNode> parentShadowNode_;
277+
std::shared_ptr<ViewShadowNode> childAShadowNode_;
278+
std::shared_ptr<ViewShadowNode> childBShadowNode_;
279+
std::shared_ptr<ViewShadowNode> childCShadowNode_;
280+
281+
YogaCloneTest() : builder_(simpleComponentBuilder()) {
282+
// clang-format off
283+
auto element =
284+
Element<RootShadowNode>()
285+
.reference(rootShadowNode_)
286+
.tag(1)
287+
.props([] {
288+
auto sharedProps = std::make_shared<RootProps>();
289+
auto &props = *sharedProps;
290+
props.layoutConstraints = LayoutConstraints{
291+
.minimumSize = {.width = 0, .height = 0},
292+
.maximumSize = {.width = 300, .height = 300}};
293+
auto &yogaStyle = props.yogaStyle;
294+
yogaStyle.setDimension(
295+
yoga::Dimension::Width, yoga::StyleSizeLength::points(300));
296+
yogaStyle.setDimension(
297+
yoga::Dimension::Height, yoga::StyleSizeLength::points(300));
298+
yogaStyle.setFlexDirection(yoga::FlexDirection::Row);
299+
return sharedProps;
300+
})
301+
.children({
302+
Element<ViewShadowNode>()
303+
.reference(parentShadowNode_)
304+
.tag(2)
305+
.props([] {
306+
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
307+
auto &props = *sharedProps;
308+
auto &yogaStyle = props.yogaStyle;
309+
yogaStyle.setFlexDirection(yoga::FlexDirection::Row);
310+
yogaStyle.setDimension(
311+
yoga::Dimension::Width,
312+
yoga::StyleSizeLength::points(300));
313+
yogaStyle.setDimension(
314+
yoga::Dimension::Height,
315+
yoga::StyleSizeLength::points(100));
316+
return sharedProps;
317+
})
318+
.children({
319+
Element<ViewShadowNode>()
320+
.reference(childAShadowNode_)
321+
.tag(3)
322+
.props([] {
323+
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
324+
sharedProps->yogaStyle.setDimension(
325+
yoga::Dimension::Width,
326+
yoga::StyleSizeLength::points(100));
327+
sharedProps->yogaStyle.setDimension(
328+
yoga::Dimension::Height,
329+
yoga::StyleSizeLength::points(100));
330+
return sharedProps;
331+
}),
332+
Element<ViewShadowNode>()
333+
.reference(childBShadowNode_)
334+
.tag(4)
335+
.props([] {
336+
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
337+
sharedProps->yogaStyle.setDimension(
338+
yoga::Dimension::Width,
339+
yoga::StyleSizeLength::points(100));
340+
sharedProps->yogaStyle.setDimension(
341+
yoga::Dimension::Height,
342+
yoga::StyleSizeLength::points(100));
343+
return sharedProps;
344+
}),
345+
Element<ViewShadowNode>()
346+
.reference(childCShadowNode_)
347+
.tag(5)
348+
.props([] {
349+
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
350+
sharedProps->yogaStyle.setDimension(
351+
yoga::Dimension::Width,
352+
yoga::StyleSizeLength::points(100));
353+
sharedProps->yogaStyle.setDimension(
354+
yoga::Dimension::Height,
355+
yoga::StyleSizeLength::points(100));
356+
return sharedProps;
357+
})
358+
})
359+
});
360+
// clang-format on
361+
362+
builder_.build(element);
363+
rootShadowNode_->layoutIfNeeded();
364+
}
365+
366+
// Bridges friend access to `YogaLayoutableShadowNode::yogaNode_` for the
367+
// `TEST_F`-generated subclasses (which inherit from this fixture and would
368+
// otherwise have to be friended individually).
369+
static yoga::Node& yogaNodeOf(const ShadowNode& shadowNode) {
370+
return static_cast<const YogaLayoutableShadowNode&>(shadowNode).yogaNode_;
371+
}
372+
};
373+
374+
// Cloning a non-leaf node WITHOUT touching the children list must produce a
375+
// subtree whose layout is byte-identical to the original (covers the
376+
// `!fragment.children` fast path that copies `yogaLayoutableChildren_` from
377+
// the source via `static_cast`).
378+
TEST_F(YogaCloneTest, unchangedChildrenClonePreservesLayout) {
379+
auto newRootShadowNode = rootShadowNode_->cloneTree(
380+
parentShadowNode_->getFamily(), [](const ShadowNode& oldShadowNode) {
381+
// Empty fragment — neither props nor children set — exercises the
382+
// `!fragment.children` fast path on the parent's clone.
383+
return oldShadowNode.clone(ShadowNodeFragment{});
384+
});
385+
386+
static_cast<RootShadowNode&>(*newRootShadowNode).layoutIfNeeded();
387+
388+
// The cloned parent must still have three layoutable children at the
389+
// expected positions — proving the copied `yogaLayoutableChildren_` is
390+
// consistent with the source.
391+
auto clonedParent = newRootShadowNode->getChildren()[0]->getChildren();
392+
ASSERT_EQ(clonedParent.size(), 3u);
393+
EXPECT_EQ(
394+
std::static_pointer_cast<const ViewShadowNode>(clonedParent[0])
395+
->getLayoutMetrics()
396+
.frame.origin.x,
397+
0);
398+
EXPECT_EQ(
399+
std::static_pointer_cast<const ViewShadowNode>(clonedParent[1])
400+
->getLayoutMetrics()
401+
.frame.origin.x,
402+
100);
403+
EXPECT_EQ(
404+
std::static_pointer_cast<const ViewShadowNode>(clonedParent[2])
405+
->getLayoutMetrics()
406+
.frame.origin.x,
407+
200);
408+
}
409+
410+
// Cloning a non-leaf node WITH a new children list must produce a subtree
411+
// whose layout reflects the new list (covers the `fragment.children` branch
412+
// where `updateYogaChildren()` rebuilds `yogaLayoutableChildren_`).
413+
TEST_F(YogaCloneTest, newChildrenCloneReflectsNewList) {
414+
// Drop the middle child.
415+
auto newChildren =
416+
std::make_shared<const std::vector<std::shared_ptr<const ShadowNode>>>(
417+
std::vector<std::shared_ptr<const ShadowNode>>{
418+
parentShadowNode_->getChildren()[0],
419+
parentShadowNode_->getChildren()[2]});
420+
421+
auto newRootShadowNode = rootShadowNode_->cloneTree(
422+
parentShadowNode_->getFamily(), [&](const ShadowNode& oldShadowNode) {
423+
return oldShadowNode.clone(
424+
{.props = ShadowNodeFragment::propsPlaceholder(),
425+
.children = newChildren});
426+
});
427+
428+
static_cast<RootShadowNode&>(*newRootShadowNode).layoutIfNeeded();
429+
430+
auto clonedParent = newRootShadowNode->getChildren()[0]->getChildren();
431+
ASSERT_EQ(clonedParent.size(), 2u);
432+
// After removing the middle child, the third original child should now sit
433+
// where the second used to be.
434+
EXPECT_EQ(
435+
std::static_pointer_cast<const ViewShadowNode>(clonedParent[0])
436+
->getLayoutMetrics()
437+
.frame.origin.x,
438+
0);
439+
EXPECT_EQ(
440+
std::static_pointer_cast<const ViewShadowNode>(clonedParent[1])
441+
->getLayoutMetrics()
442+
.frame.origin.x,
443+
100);
444+
}
445+
446+
// Successive prop-only clones (the animation commit pattern) must each
447+
// produce a self-consistent layout. Validates that the fast path's vector
448+
// copy yields a vector the next clone can again copy from.
449+
TEST_F(YogaCloneTest, repeatedUnchangedChildrenClonesYieldStableLayout) {
450+
std::shared_ptr<const RootShadowNode> current = rootShadowNode_;
451+
for (int i = 0; i < 5; i++) {
452+
auto next = current->cloneTree(
453+
parentShadowNode_->getFamily(), [](const ShadowNode& oldShadowNode) {
454+
return oldShadowNode.clone(ShadowNodeFragment{});
455+
});
456+
auto& mutableNext = static_cast<RootShadowNode&>(*next);
457+
mutableNext.layoutIfNeeded();
458+
current = std::static_pointer_cast<const RootShadowNode>(next);
459+
460+
auto parentChildren = current->getChildren()[0]->getChildren();
461+
ASSERT_EQ(parentChildren.size(), 3u);
462+
EXPECT_EQ(
463+
std::static_pointer_cast<const ViewShadowNode>(parentChildren[2])
464+
->getLayoutMetrics()
465+
.frame.origin.x,
466+
200);
467+
}
468+
}
469+
470+
// The fast path copies the source's `yogaLayoutableChildren_` vector; that
471+
// copy must be independent — mutating the clone via `appendChild` must not
472+
// disturb the source's vector. Indirect signal: source still lays out with
473+
// three children, clone with four.
474+
TEST_F(YogaCloneTest, appendChildOnCloneDoesNotAffectSource) {
475+
auto clonedParent = std::static_pointer_cast<ViewShadowNode>(
476+
parentShadowNode_->clone(ShadowNodeFragment{}));
477+
478+
auto extraChild = std::static_pointer_cast<ViewShadowNode>(
479+
childCShadowNode_->clone(ShadowNodeFragment{}));
480+
clonedParent->appendChild(extraChild);
481+
482+
EXPECT_EQ(parentShadowNode_->getChildren().size(), 3u);
483+
EXPECT_EQ(clonedParent->getChildren().size(), 4u);
484+
485+
// Source's layout (already computed in the fixture) is still valid because
486+
// its yoga subtree was untouched.
487+
EXPECT_EQ(childCShadowNode_->getLayoutMetrics().frame.origin.x, 200);
488+
}
489+
490+
// After cloning a non-leaf node with `!fragment.children`, the cloned
491+
// parent's `yogaNode_` is copy-constructed from the source's and therefore
492+
// shares the same child yoga-node pointers (no synchronous re-parenting,
493+
// no yoga-side reallocation). Yoga's clone callback handles ownership
494+
// transfer lazily on the cloned subtree's first layout, so the source must
495+
// remain undisturbed — both its child count AND its ownership of each
496+
// child yoga node are preserved.
497+
TEST_F(YogaCloneTest, cloneInheritsSourceYogaChildrenWithoutDisturbance) {
498+
const size_t sourceChildCount =
499+
yogaNodeOf(*parentShadowNode_).getChildCount();
500+
// Snapshot each child's owner before the clone.
501+
std::vector<yoga::Node*> ownersBefore;
502+
for (const auto& child : parentShadowNode_->getChildren()) {
503+
ownersBefore.push_back(yogaNodeOf(*child).getOwner());
504+
EXPECT_EQ(ownersBefore.back(), &yogaNodeOf(*parentShadowNode_))
505+
<< "Sanity: source owns its children before clone";
506+
}
507+
508+
auto clonedParent = std::static_pointer_cast<ViewShadowNode>(
509+
parentShadowNode_->clone(ShadowNodeFragment{}));
510+
511+
// Cloned parent inherits source's child yoga-node references — same count,
512+
// same pointers — without running `updateYogaChildren()`.
513+
ASSERT_EQ(yogaNodeOf(*clonedParent).getChildCount(), sourceChildCount);
514+
for (size_t i = 0; i < sourceChildCount; i++) {
515+
EXPECT_EQ(
516+
yogaNodeOf(*clonedParent).getChild(i),
517+
yogaNodeOf(*parentShadowNode_).getChild(i))
518+
<< "Cloned parent's yoga child at index " << i
519+
<< " must alias the source's";
520+
}
521+
522+
// Source's ownership of every child yoga node is unchanged by the clone.
523+
for (size_t i = 0; i < parentShadowNode_->getChildren().size(); i++) {
524+
const auto& child = *parentShadowNode_->getChildren()[i];
525+
EXPECT_EQ(yogaNodeOf(child).getOwner(), ownersBefore[i])
526+
<< "Source's yoga ownership must be preserved across clone";
527+
}
528+
}
529+
530+
// `cloneTree` clones every node on the path from root to the target, AND
531+
// every sibling on that path — because Fabric forces ownership transfer via
532+
// `adoptYogaChild`, which clones any child whose yoga node is still owned by
533+
// the previous parent. Grandchildren below an unchanged sibling are NOT
534+
// touched (structural sharing kicks in there).
535+
//
536+
// This is the actual "number of clones" invariant for Fabric's cloneTree.
537+
TEST_F(YogaCloneTest, cloneTreeClonesPathPlusSiblings) {
538+
// Build a four-level tree so we have a grandchild we can pointer-compare
539+
// against under an unchanged sibling.
540+
std::shared_ptr<ViewShadowNode> grandchildOfA;
541+
std::shared_ptr<RootShadowNode> deepRoot;
542+
std::shared_ptr<ViewShadowNode> deepParent;
543+
std::shared_ptr<ViewShadowNode> deepChildA;
544+
std::shared_ptr<ViewShadowNode> deepChildB;
545+
// clang-format off
546+
auto element =
547+
Element<RootShadowNode>()
548+
.reference(deepRoot)
549+
.tag(101)
550+
.props([] {
551+
auto sharedProps = std::make_shared<RootProps>();
552+
sharedProps->layoutConstraints = LayoutConstraints{
553+
.minimumSize = {.width = 0, .height = 0},
554+
.maximumSize = {.width = 300, .height = 300}};
555+
return sharedProps;
556+
})
557+
.children({
558+
Element<ViewShadowNode>()
559+
.reference(deepParent)
560+
.tag(102)
561+
.children({
562+
Element<ViewShadowNode>()
563+
.reference(deepChildA)
564+
.tag(103)
565+
.children({
566+
Element<ViewShadowNode>()
567+
.reference(grandchildOfA)
568+
.tag(104)
569+
}),
570+
Element<ViewShadowNode>()
571+
.reference(deepChildB)
572+
.tag(105)
573+
})
574+
});
575+
// clang-format on
576+
builder_.build(element);
577+
deepRoot->layoutIfNeeded();
578+
579+
const ShadowNode* origRoot = deepRoot.get();
580+
const ShadowNode* origParent = deepParent.get();
581+
const ShadowNode* origChildA = deepChildA.get();
582+
const ShadowNode* origChildB = deepChildB.get();
583+
const ShadowNode* origGrandchildA = grandchildOfA.get();
584+
585+
// Target childB. Path: root → parent → childB.
586+
auto newRoot = deepRoot->cloneTree(
587+
deepChildB->getFamily(), [](const ShadowNode& oldShadowNode) {
588+
return oldShadowNode.clone(ShadowNodeFragment{});
589+
});
590+
591+
const ShadowNode* newRootPtr = newRoot.get();
592+
const ShadowNode* newParent = newRoot->getChildren()[0].get();
593+
const ShadowNode* newChildA = newParent->getChildren()[0].get();
594+
const ShadowNode* newChildB = newParent->getChildren()[1].get();
595+
const ShadowNode* newGrandchildA = newChildA->getChildren()[0].get();
596+
597+
// Path from root to target: every node is a fresh allocation.
598+
EXPECT_NE(newRootPtr, origRoot) << "Root cloned (on path)";
599+
EXPECT_NE(newParent, origParent) << "Parent cloned (on path)";
600+
EXPECT_NE(newChildB, origChildB) << "Target cloned";
601+
602+
// Siblings of the target ARE cloned in Fabric, because parent's
603+
// updateYogaChildren() → adoptYogaChild() clones any child whose yoga
604+
// node is still owned by the previous parent.
605+
EXPECT_NE(newChildA, origChildA)
606+
<< "Sibling cloned to take new yoga ownership";
607+
608+
// But the sibling's children are NOT cloned — structural sharing kicks in
609+
// one level below the disturbance.
610+
EXPECT_EQ(newGrandchildA, origGrandchildA)
611+
<< "Grandchild under unchanged sibling must be shared";
612+
}
613+
266614
} // namespace facebook::react

0 commit comments

Comments
 (0)