|
17 | 17 | #include <react/renderer/core/PropsParserContext.h> |
18 | 18 | #include <react/renderer/element/ComponentBuilder.h> |
19 | 19 |
|
| 20 | +#include <react/renderer/components/view/YogaLayoutableShadowNode.h> |
20 | 21 | #include <react/renderer/element/Element.h> |
21 | 22 | #include <react/renderer/element/testUtils.h> |
| 23 | +#include <yoga/node/Node.h> |
22 | 24 | #include <yoga/numeric/FloatOptional.h> |
23 | 25 |
|
24 | 26 | namespace facebook::react { |
@@ -263,4 +265,350 @@ TEST_F(YogaDirtyFlagTest, clonedPropsPreserveAspectRatio) { |
263 | 265 | }); |
264 | 266 | } |
265 | 267 |
|
| 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 | + |
266 | 614 | } // namespace facebook::react |
0 commit comments