feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,36 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
add_compile_options(
-fexceptions
-frtti
-std=c++20
-Wall
-Wpedantic
-DLOG_TAG=\"Fabric\")
file(GLOB react_render_mounting_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_render_mounting STATIC ${react_render_mounting_SRC})
target_include_directories(react_render_mounting PRIVATE .)
target_include_directories(react_render_mounting PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_render_mounting
folly_runtime
glog
glog_init
jsi
react_debug
react_render_core
react_render_debug
react_render_graphics
react_render_telemetry
react_utils
rrc_root
rrc_view
yoga)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/debug/flags.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <deque>
namespace facebook::react {
enum class ReparentMode { Flatten, Unflatten };
/**
* During differ, we need to keep some `ShadowViewNodePair`s in memory.
* Some `ShadowViewNodePair`s are referenced from std::vectors returned
* by `sliceChildShadowNodeViewPairsV2`; some are referenced in TinyMaps
* for view (un)flattening especially; and it is not always clear which
* std::vectors will outlive which TinyMaps, and vice-versa, so it doesn't
* make sense for the std::vector or TinyMap to own any `ShadowViewNodePair`s.
*
* Thus, we introduce the concept of a scope.
*
* For the duration of some operation, we keep a ViewNodePairScope around, such
* that: (1) the ViewNodePairScope keeps each
* ShadowViewNodePair alive, (2) we have a stable pointer value that we can
* use to reference each ShadowViewNodePair (not guaranteed with std::vector,
* for example, which may have to resize and move values around).
*
* As long as we only manipulate the data-structure with push_back, std::deque
* both (1) ensures that pointers into the data-structure are never invalidated,
* and (2) tries to efficiently allocate storage such that as many objects as
* possible are close in memory, but does not guarantee adjacency.
*/
using ViewNodePairScope = std::deque<ShadowViewNodePair>;
/*
* Calculates a list of view mutations which describes how the old
* `ShadowTree` can be transformed to the new one.
* The list of mutations might be and might not be optimal.
*/
ShadowViewMutation::List calculateShadowViewMutations(
const ShadowNode& oldRootShadowNode,
const ShadowNode& newRootShadowNode);
/**
* Generates a list of `ShadowViewNodePair`s that represents a layer of a
* flattened view hierarchy. The V2 version preserves nodes even if they do
* not form views and their children are flattened.
*/
ShadowViewNodePair::NonOwningList sliceChildShadowNodeViewPairsV2(
const ShadowNode& shadowNode,
ViewNodePairScope& viewNodePairScope,
bool allowFlattened = false,
Point layoutOffset = {0, 0});
/*
* Generates a list of `ShadowViewNodePair`s that represents a layer of a
* flattened view hierarchy. This is *only* used by unit tests currently.
*/
ShadowViewNodePair::OwningList sliceChildShadowNodeViewPairsForTesting(
const ShadowNode& shadowNode);
} // namespace facebook::react

View File

@@ -0,0 +1,199 @@
/*
* 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 "MountingCoordinator.h"
#ifdef RN_SHADOW_TREE_INTROSPECTION
#include <glog/logging.h>
#include <sstream>
#endif
#include <condition_variable>
#include <react/debug/react_native_assert.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
namespace facebook::react {
MountingCoordinator::MountingCoordinator(const ShadowTreeRevision& baseRevision)
: surfaceId_(baseRevision.rootShadowNode->getSurfaceId()),
baseRevision_(baseRevision),
telemetryController_(*this) {
#ifdef RN_SHADOW_TREE_INTROSPECTION
stubViewTree_ = buildStubViewTreeWithoutUsingDifferentiator(
*baseRevision_.rootShadowNode);
#endif
}
SurfaceId MountingCoordinator::getSurfaceId() const {
return surfaceId_;
}
void MountingCoordinator::push(ShadowTreeRevision revision) const {
{
std::scoped_lock lock(mutex_);
react_native_assert(
!lastRevision_.has_value() || revision.number != lastRevision_->number);
if (!lastRevision_.has_value() || lastRevision_->number < revision.number) {
lastRevision_ = std::move(revision);
}
}
signal_.notify_all();
}
void MountingCoordinator::revoke() const {
std::scoped_lock lock(mutex_);
// We have two goals here.
// 1. We need to stop retaining `ShadowNode`s to not prolong their lifetime
// to prevent them from overliving `ComponentDescriptor`s.
// 2. A possible call to `pullTransaction()` should return empty optional.
baseRevision_.rootShadowNode.reset();
lastRevision_.reset();
}
bool MountingCoordinator::waitForTransaction(
std::chrono::duration<double> timeout) const {
std::unique_lock<std::mutex> lock(mutex_);
return signal_.wait_for(
lock, timeout, [this]() { return lastRevision_.has_value(); });
}
void MountingCoordinator::updateBaseRevision(
const ShadowTreeRevision& baseRevision) const {
baseRevision_ = baseRevision;
}
void MountingCoordinator::resetLatestRevision() const {
lastRevision_.reset();
}
std::optional<MountingTransaction> MountingCoordinator::pullTransaction()
const {
SystraceSection section("MountingCoordinator::pullTransaction");
std::scoped_lock lock(mutex_);
auto transaction = std::optional<MountingTransaction>{};
// Base case
if (lastRevision_.has_value()) {
number_++;
auto telemetry = lastRevision_->telemetry;
telemetry.willDiff();
auto mutations = calculateShadowViewMutations(
*baseRevision_.rootShadowNode, *lastRevision_->rootShadowNode);
telemetry.didDiff();
transaction = MountingTransaction{
surfaceId_, number_, std::move(mutations), telemetry};
}
// Override case
auto mountingOverrideDelegate = mountingOverrideDelegate_.lock();
auto shouldOverridePullTransaction = mountingOverrideDelegate &&
mountingOverrideDelegate->shouldOverridePullTransaction();
if (shouldOverridePullTransaction) {
auto mutations = ShadowViewMutation::List{};
auto telemetry = TransactionTelemetry{};
if (transaction.has_value()) {
mutations = transaction->getMutations();
telemetry = transaction->getTelemetry();
} else {
number_++;
telemetry.willLayout();
telemetry.didLayout();
telemetry.willCommit();
telemetry.didCommit();
telemetry.willDiff();
telemetry.didDiff();
}
transaction = mountingOverrideDelegate->pullTransaction(
surfaceId_, number_, telemetry, std::move(mutations));
}
#ifdef RN_SHADOW_TREE_INTROSPECTION
if (transaction.has_value()) {
// We have something to validate.
auto mutations = transaction->getMutations();
// No matter what the source of the transaction is, it must be able to
// mutate the existing stub view tree.
stubViewTree_.mutate(mutations);
// If the transaction was overridden, we don't have a model of the shadow
// tree therefore we cannot validate the validity of the mutation
// instructions.
if (!shouldOverridePullTransaction && lastRevision_.has_value()) {
auto stubViewTree = buildStubViewTreeWithoutUsingDifferentiator(
*lastRevision_->rootShadowNode);
bool treesEqual = stubViewTree_ == stubViewTree;
if (!treesEqual) {
// Display debug info
auto line = std::string{};
std::stringstream ssOldTree(
baseRevision_.rootShadowNode->getDebugDescription());
while (std::getline(ssOldTree, line, '\n')) {
LOG(ERROR) << "Old tree:" << line;
}
std::stringstream ssMutations(getDebugDescription(mutations, {}));
while (std::getline(ssMutations, line, '\n')) {
LOG(ERROR) << "Mutations:" << line;
}
std::stringstream ssNewTree(
lastRevision_->rootShadowNode->getDebugDescription());
while (std::getline(ssNewTree, line, '\n')) {
LOG(ERROR) << "New tree:" << line;
}
}
react_native_assert(
(treesEqual) && "Incorrect set of mutations detected.");
}
}
#endif
if (lastRevision_.has_value()) {
baseRevision_ = std::move(*lastRevision_);
lastRevision_.reset();
}
return transaction;
}
bool MountingCoordinator::hasPendingTransactions() const {
return lastRevision_.has_value();
}
const TelemetryController& MountingCoordinator::getTelemetryController() const {
return telemetryController_;
}
const ShadowTreeRevision& MountingCoordinator::getBaseRevision() const {
return baseRevision_;
}
void MountingCoordinator::setMountingOverrideDelegate(
std::weak_ptr<const MountingOverrideDelegate> delegate) const {
std::scoped_lock lock(mutex_);
mountingOverrideDelegate_ = std::move(delegate);
}
} // namespace facebook::react

View File

@@ -0,0 +1,130 @@
/*
* 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.
*/
#pragma once
#include <chrono>
#include <condition_variable>
#include <optional>
#include <react/renderer/debug/flags.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/MountingOverrideDelegate.h>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/renderer/mounting/TelemetryController.h>
#include "ShadowTreeRevision.h"
#ifdef RN_SHADOW_TREE_INTROSPECTION
#include <react/renderer/mounting/stubs.h>
#endif
namespace facebook::react {
/*
* Stores inside all non-mounted yet revisions of a shadow tree and coordinates
* mounting. The object stores the most recent mounted revision and the most
* recent committed one. Then when a new mounting transaction is requested the
* object generates mutation instructions and returns it as a
* `MountingTransaction`.
*/
class MountingCoordinator final {
public:
using Shared = std::shared_ptr<const MountingCoordinator>;
/*
* The constructor is meant to be used only inside `ShadowTree`, and it's
* `public` only to enable using with `std::make_shared<>`.
*/
MountingCoordinator(const ShadowTreeRevision& baseRevision);
/*
* Returns the id of the surface that the coordinator belongs to.
*/
SurfaceId getSurfaceId() const;
/*
* Computes a consequent mounting transaction and returns it.
* The returning transaction can accumulate multiple recent revisions of a
* shadow tree. Returns empty optional if there no new shadow tree revision to
* mount.
* The method is thread-safe and can be called from any thread.
* However, a consumer should always call it on the same thread (e.g. on the
* main thread) or ensure sequentiality of mount transactions separately.
*/
std::optional<MountingTransaction> pullTransaction() const;
/*
* Indicates if there are transactions waiting to be consumed and mounted on
* the host platform. This can be useful to determine if side-effects of
* mounting can be expected after some operations (like IntersectionObserver
* initial paint notifications).
*/
bool hasPendingTransactions() const;
/*
* Blocks the current thread until a new mounting transaction is available or
* after the specified `timeout` duration.
* Returns `false` if a timeout occurred before a new transaction available.
* Call `pullTransaction` right after the method to retrieve the transaction.
* Similarly to `pullTransaction` this method is thread-safe but the consumer
* should call it on the same thread (e.g. on the main thread) or ensure
* sequentiality of mount transactions separately.
*/
bool waitForTransaction(std::chrono::duration<double> timeout) const;
const TelemetryController& getTelemetryController() const;
const ShadowTreeRevision& getBaseRevision() const;
/*
* Methods from this section are meant to be used by
* `MountingOverrideDelegate` only.
*/
public:
void updateBaseRevision(const ShadowTreeRevision& baseRevision) const;
void resetLatestRevision() const;
void setMountingOverrideDelegate(
std::weak_ptr<const MountingOverrideDelegate> delegate) const;
/*
* Methods from this section are meant to be used by `ShadowTree` only.
*/
private:
friend class ShadowTree;
void push(ShadowTreeRevision revision) const;
/*
* Revokes the last pushed `ShadowTreeRevision`.
* Generating a `MountingTransaction` requires some resources which the
* `MountingCoordinator` does not own (e.g. `ComponentDescriptor`s). Revoking
* committed revisions allows the owner (a Shadow Tree) to make sure that
* those resources will not be accessed (e.g. by the Mounting Layer).
*/
void revoke() const;
private:
const SurfaceId surfaceId_;
mutable std::mutex mutex_;
mutable ShadowTreeRevision baseRevision_;
mutable std::optional<ShadowTreeRevision> lastRevision_{};
mutable MountingTransaction::Number number_{0};
mutable std::condition_variable signal_;
mutable std::weak_ptr<const MountingOverrideDelegate>
mountingOverrideDelegate_;
TelemetryController telemetryController_;
#ifdef RN_SHADOW_TREE_INTROSPECTION
mutable StubViewTree stubViewTree_; // Protected by `mutex_`.
#endif
};
} // namespace facebook::react

View File

@@ -0,0 +1,45 @@
/*
* 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 <react/renderer/mounting/MountingTransaction.h>
#pragma once
namespace facebook::react {
class MountingCoordinator;
/**
* Generic interface for anything that needs to override specific
* MountingCoordinator methods. This is for platform-specific escape hatches
* like animations.
*/
class MountingOverrideDelegate {
public:
virtual bool shouldOverridePullTransaction() const = 0;
virtual ~MountingOverrideDelegate() = default;
/**
* Delegates that override this method are responsible for:
*
* - Returning a MountingTransaction with mutations
* - Calling
* - Telemetry, if appropriate
*
* @param surfaceId
* @param number
* @param mountingCoordinator
* @return
*/
virtual std::optional<MountingTransaction> pullTransaction(
SurfaceId surfaceId,
MountingTransaction::Number number,
const TransactionTelemetry& telemetry,
ShadowViewMutationList mutations) const = 0;
};
} // namespace facebook::react

View File

@@ -0,0 +1,44 @@
/*
* 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 "MountingTransaction.h"
namespace facebook::react {
using Number = MountingTransaction::Number;
MountingTransaction::MountingTransaction(
SurfaceId surfaceId,
Number number,
ShadowViewMutationList&& mutations,
TransactionTelemetry telemetry)
: surfaceId_(surfaceId),
number_(number),
mutations_(std::move(mutations)),
telemetry_(std::move(telemetry)) {}
const ShadowViewMutationList& MountingTransaction::getMutations() const& {
return mutations_;
}
ShadowViewMutationList MountingTransaction::getMutations() && {
return std::move(mutations_);
}
TransactionTelemetry& MountingTransaction::getTelemetry() const {
return telemetry_;
}
SurfaceId MountingTransaction::getSurfaceId() const {
return surfaceId_;
}
Number MountingTransaction::getNumber() const {
return number_;
}
} // namespace facebook::react

View File

@@ -0,0 +1,86 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/SurfaceTelemetry.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
/*
* Encapsulates all artifacts of `ShadowTree` commit (or a series of them),
* particularly list of mutations and meta-data associated with the commit.
* Movable and copyable, but moving is strongly encouraged.
* Beware: A moved-from object of this type has unspecified value and accessing
* that is UB (Undefined Behaviour).
*/
class MountingTransaction final {
public:
/*
* A Number (or revision) grows continuously starting from `1`. Value `0`
* represents the state before the very first transaction happens.
*/
using Number = int64_t;
/*
* Copying a list of `ShadowViewMutation` is expensive, so the constructor
* accepts it as rvalue reference to discourage copying.
*/
MountingTransaction(
SurfaceId surfaceId,
Number number,
ShadowViewMutationList&& mutations,
TransactionTelemetry telemetry);
/*
* Copy semantic.
* Copying of MountingTransaction is expensive, so copy-constructor is
* explicit and copy-assignment is deleted to prevent accidental copying.
*/
explicit MountingTransaction(const MountingTransaction& mountingTransaction) =
default;
MountingTransaction& operator=(const MountingTransaction& other) = delete;
/*
* Move semantic.
*/
MountingTransaction(MountingTransaction&& mountingTransaction) noexcept =
default;
MountingTransaction& operator=(MountingTransaction&& other) = default;
/*
* Returns a list of mutations that represent the transaction. The list can be
* empty (theoretically).
*/
const ShadowViewMutationList& getMutations() const&;
ShadowViewMutationList getMutations() &&;
/*
* Returns telemetry associated with this transaction.
*/
TransactionTelemetry& getTelemetry() const;
/*
* Returns the id of the surface that the transaction belongs to.
*/
SurfaceId getSurfaceId() const;
/*
* Returns a sequential number of the particular transaction.
*/
Number getNumber() const;
private:
SurfaceId surfaceId_;
Number number_;
ShadowViewMutationList mutations_;
mutable TransactionTelemetry telemetry_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,543 @@
/*
* 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 "ShadowTree.h"
#include <react/debug/react_native_assert.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/core/LayoutPrimitives.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
#include <react/utils/CoreFeatures.h>
#include "ShadowTreeDelegate.h"
namespace facebook::react {
using CommitStatus = ShadowTree::CommitStatus;
using CommitMode = ShadowTree::CommitMode;
// --- Clone-less progress state algorithm ---
// Note: Ideally, we don't have to const_cast but our use of constness in
// C++ is overly restrictive. We do const_cast here but the only place where
// we change ShadowNode is by calling `ShadowNode::progressStateIfNecessary`
// where checks are in place to avoid manipulating a sealed ShadowNode.
static void progressStateIfNecessary(ShadowNode& newShadowNode) {
newShadowNode.progressStateIfNecessary();
for (const auto& childNode : newShadowNode.getChildren()) {
progressStateIfNecessary(const_cast<ShadowNode&>(*childNode));
}
}
static void progressStateIfNecessary(
ShadowNode& newShadowNode,
const ShadowNode& baseShadowNode) {
newShadowNode.progressStateIfNecessary();
auto& newChildren = newShadowNode.getChildren();
auto& baseChildren = baseShadowNode.getChildren();
auto newChildrenSize = newChildren.size();
auto baseChildrenSize = baseChildren.size();
auto index = size_t{0};
for (index = 0; index < newChildrenSize && index < baseChildrenSize;
++index) {
const auto& newChildNode = *newChildren[index];
const auto& baseChildNode = *baseChildren[index];
if (&newChildNode == &baseChildNode) {
// Nodes are identical. They are shared between `newShadowNode` and
// `baseShadowNode` and it is safe to skipping.
continue;
}
if (!ShadowNode::sameFamily(newChildNode, baseChildNode)) {
// The nodes are not of the same family. Tree hierarchy has changed
// and we have to fall back to full sub-tree traversal from this point on.
break;
}
progressStateIfNecessary(
const_cast<ShadowNode&>(newChildNode), baseChildNode);
}
for (; index < newChildrenSize; ++index) {
const auto& newChildNode = *newChildren[index];
progressStateIfNecessary(const_cast<ShadowNode&>(newChildNode));
}
}
// --- End of Clone-less progress state algorithm ---
/*
* Generates (possibly) a new tree where all nodes with non-obsolete `State`
* objects. If all `State` objects in the tree are not obsolete for the moment
* of calling, the function returns `nullptr` (as an indication that no
* additional work is required).
*/
static ShadowNode::Unshared progressState(const ShadowNode& shadowNode) {
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto newChildren = ShadowNode::ListOfShared{};
if (!shadowNode.getChildren().empty()) {
auto index = size_t{0};
for (const auto& childNode : shadowNode.getChildren()) {
auto newChildNode = progressState(*childNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = shadowNode.getChildren();
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
index++;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
ShadowNodeFragment::propsPlaceholder(),
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
/*
* An optimized version of the previous function (and relies on it).
* The function uses a given base tree to exclude unchanged (equal) parts
* of the three from the traversing.
*/
static ShadowNode::Unshared progressState(
const ShadowNode& shadowNode,
const ShadowNode& baseShadowNode) {
// The intuition behind the complexity:
// - A very few nodes have associated state, therefore it's mostly reading and
// it only writes when state objects were found obsolete;
// - Most before-after trees are aligned, therefore most tree branches will be
// skipped;
// - If trees are significantly different, any other algorithm will have
// close to linear complexity.
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto& children = shadowNode.getChildren();
auto& baseChildren = baseShadowNode.getChildren();
auto newChildren = ShadowNode::ListOfShared{};
auto childrenSize = children.size();
auto baseChildrenSize = baseChildren.size();
auto index = size_t{0};
// Stage 1: Aligned part.
for (index = 0; index < childrenSize && index < baseChildrenSize; index++) {
const auto& childNode = *children[index];
const auto& baseChildNode = *baseChildren[index];
if (&childNode == &baseChildNode) {
// Nodes are identical, skipping.
continue;
}
if (!ShadowNode::sameFamily(childNode, baseChildNode)) {
// Totally different nodes, updating is impossible.
break;
}
auto newChildNode = progressState(childNode, baseChildNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
// Stage 2: Misaligned part.
for (; index < childrenSize; index++) {
auto newChildNode = progressState(*children[index]);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
ShadowNodeFragment::propsPlaceholder(),
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
static void updateMountedFlag(
const ShadowNode::ListOfShared& oldChildren,
const ShadowNode::ListOfShared& newChildren) {
// This is a simplified version of Diffing algorithm that only updates
// `mounted` flag on `ShadowNode`s. The algorithm sets "mounted" flag before
// "unmounted" to allow `ShadowNode` detect a situation where the node was
// remounted.
if (&oldChildren == &newChildren) {
// Lists are identical, nothing to do.
return;
}
if (oldChildren.empty() && newChildren.empty()) {
// Both lists are empty, nothing to do.
return;
}
size_t index;
// Stage 1: Mount and unmount "updated" children.
for (index = 0; index < oldChildren.size() && index < newChildren.size();
index++) {
const auto& oldChild = oldChildren[index];
const auto& newChild = newChildren[index];
if (oldChild == newChild) {
// Nodes are identical, skipping the subtree.
continue;
}
if (!ShadowNode::sameFamily(*oldChild, *newChild)) {
// Totally different nodes, updating is impossible.
break;
}
newChild->setMounted(true);
oldChild->setMounted(false);
updateMountedFlag(oldChild->getChildren(), newChild->getChildren());
}
size_t lastIndexAfterFirstStage = index;
// State 2: Mount new children.
for (index = lastIndexAfterFirstStage; index < newChildren.size(); index++) {
const auto& newChild = newChildren[index];
newChild->setMounted(true);
updateMountedFlag({}, newChild->getChildren());
}
// State 3: Unmount old children.
for (index = lastIndexAfterFirstStage; index < oldChildren.size(); index++) {
const auto& oldChild = oldChildren[index];
oldChild->setMounted(false);
updateMountedFlag(oldChild->getChildren(), {});
}
}
ShadowTree::ShadowTree(
SurfaceId surfaceId,
const LayoutConstraints& layoutConstraints,
const LayoutContext& layoutContext,
const ShadowTreeDelegate& delegate,
const ContextContainer& contextContainer)
: surfaceId_(surfaceId), delegate_(delegate) {
static auto globalRootComponentDescriptor =
std::make_unique<const RootComponentDescriptor>(
ComponentDescriptorParameters{
EventDispatcher::Shared{}, nullptr, nullptr});
const auto props = std::make_shared<const RootProps>(
PropsParserContext{surfaceId, contextContainer},
*RootShadowNode::defaultSharedProps(),
layoutConstraints,
layoutContext);
auto family = globalRootComponentDescriptor->createFamily(
{surfaceId, surfaceId, nullptr});
auto rootShadowNode = std::static_pointer_cast<const RootShadowNode>(
globalRootComponentDescriptor->createShadowNode(
ShadowNodeFragment{
/* .props = */ props,
},
family));
currentRevision_ = ShadowTreeRevision{
rootShadowNode, INITIAL_REVISION, TransactionTelemetry{}};
lastRevisionNumberWithNewState_ = currentRevision_.number;
mountingCoordinator_ =
std::make_shared<const MountingCoordinator>(currentRevision_);
}
ShadowTree::~ShadowTree() {
mountingCoordinator_->revoke();
}
Tag ShadowTree::getSurfaceId() const {
return surfaceId_;
}
void ShadowTree::setCommitMode(CommitMode commitMode) const {
auto revision = ShadowTreeRevision{};
{
std::unique_lock lock(commitMutex_);
if (commitMode_ == commitMode) {
return;
}
commitMode_ = commitMode;
revision = currentRevision_;
}
// initial revision never contains any commits so mounting it here is
// incorrect
if (commitMode == CommitMode::Normal && revision.number != INITIAL_REVISION) {
mount(revision, true);
}
}
CommitMode ShadowTree::getCommitMode() const {
std::shared_lock lock(commitMutex_);
return commitMode_;
}
MountingCoordinator::Shared ShadowTree::getMountingCoordinator() const {
return mountingCoordinator_;
}
CommitStatus ShadowTree::commit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
SystraceSection s("ShadowTree::commit");
[[maybe_unused]] int attempts = 0;
while (true) {
attempts++;
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
}
// After multiple attempts, we failed to commit the transaction.
// Something internally went terribly wrong.
react_native_assert(attempts < 1024);
}
}
CommitStatus ShadowTree::tryCommit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
SystraceSection s("ShadowTree::tryCommit");
auto telemetry = TransactionTelemetry{};
telemetry.willCommit();
CommitMode commitMode;
auto oldRevision = ShadowTreeRevision{};
auto newRevision = ShadowTreeRevision{};
ShadowTreeRevision::Number lastRevisionNumberWithNewState;
{
// Reading `currentRevision_` in shared manner.
std::shared_lock lock(commitMutex_);
commitMode = commitMode_;
oldRevision = currentRevision_;
lastRevisionNumberWithNewState = lastRevisionNumberWithNewState_;
}
const auto& oldRootShadowNode = oldRevision.rootShadowNode;
auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);
if (!newRootShadowNode ||
(commitOptions.shouldYield && commitOptions.shouldYield())) {
return CommitStatus::Cancelled;
}
if (commitOptions.enableStateReconciliation) {
if (CoreFeatures::enableClonelessStateProgression) {
progressStateIfNecessary(*newRootShadowNode, *oldRootShadowNode);
} else {
auto updatedNewRootShadowNode =
progressState(*newRootShadowNode, *oldRootShadowNode);
if (updatedNewRootShadowNode) {
newRootShadowNode =
std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
}
}
}
// Run commit hooks.
newRootShadowNode = delegate_.shadowTreeWillCommit(
*this, oldRootShadowNode, newRootShadowNode);
if (!newRootShadowNode ||
(commitOptions.shouldYield && commitOptions.shouldYield())) {
return CommitStatus::Cancelled;
}
// Layout nodes.
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
telemetry.willLayout();
telemetry.setAsThreadLocal();
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
telemetry.unsetAsThreadLocal();
telemetry.didLayout(static_cast<int>(affectedLayoutableNodes.size()));
{
// Updating `currentRevision_` in unique manner if it hasn't changed.
std::unique_lock lock(commitMutex_);
if (commitOptions.shouldYield && commitOptions.shouldYield()) {
return CommitStatus::Cancelled;
}
if (CoreFeatures::enableGranularShadowTreeStateReconciliation) {
auto lastRevisionNumberWithNewStateChanged =
lastRevisionNumberWithNewState != lastRevisionNumberWithNewState_;
// Commit should only fail if we propagated the wrong state.
if (commitOptions.enableStateReconciliation &&
lastRevisionNumberWithNewStateChanged) {
return CommitStatus::Failed;
}
} else {
if (currentRevision_.number != oldRevision.number) {
return CommitStatus::Failed;
}
}
auto newRevisionNumber = currentRevision_.number + 1;
{
std::scoped_lock dispatchLock(EventEmitter::DispatchMutex());
updateMountedFlag(
currentRevision_.rootShadowNode->getChildren(),
newRootShadowNode->getChildren());
}
telemetry.didCommit();
telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));
// Seal the shadow node so it can no longer be mutated
newRootShadowNode->sealRecursive();
newRevision = ShadowTreeRevision{
std::move(newRootShadowNode), newRevisionNumber, telemetry};
currentRevision_ = newRevision;
if (!commitOptions.enableStateReconciliation) {
lastRevisionNumberWithNewState_ = newRevisionNumber;
}
}
emitLayoutEvents(affectedLayoutableNodes);
if (commitMode == CommitMode::Normal) {
mount(std::move(newRevision), commitOptions.mountSynchronously);
}
return CommitStatus::Succeeded;
}
ShadowTreeRevision ShadowTree::getCurrentRevision() const {
std::shared_lock lock(commitMutex_);
return currentRevision_;
}
void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously)
const {
mountingCoordinator_->push(std::move(revision));
delegate_.shadowTreeDidFinishTransaction(
mountingCoordinator_, mountSynchronously);
}
void ShadowTree::commitEmptyTree() const {
commit(
[](const RootShadowNode& oldRootShadowNode) -> RootShadowNode::Unshared {
return std::make_shared<RootShadowNode>(
oldRootShadowNode,
ShadowNodeFragment{
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
/* .children = */ ShadowNode::emptySharedShadowNodeSharedList(),
});
},
{/* default commit options */});
}
void ShadowTree::emitLayoutEvents(
std::vector<const LayoutableShadowNode*>& affectedLayoutableNodes) const {
SystraceSection s(
"ShadowTree::emitLayoutEvents",
"affectedLayoutableNodes",
affectedLayoutableNodes.size());
for (const auto* layoutableNode : affectedLayoutableNodes) {
// Only instances of `ViewShadowNode` (and subclasses) are supported.
const auto& viewShadowNode =
static_cast<const ViewShadowNode&>(*layoutableNode);
const auto& viewEventEmitter =
static_cast<const ViewEventEmitter&>(*viewShadowNode.getEventEmitter());
// Checking if the `onLayout` event was requested for the particular Shadow
// Node.
const auto& viewProps =
static_cast<const ViewProps&>(*viewShadowNode.getProps());
if (!viewProps.onLayout) {
continue;
}
viewEventEmitter.onLayout(layoutableNode->getLayoutMetrics());
}
}
void ShadowTree::notifyDelegatesOfUpdates() const {
delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true);
}
} // namespace facebook::react

View File

@@ -0,0 +1,155 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/MountingCoordinator.h>
#include <react/renderer/mounting/ShadowTreeDelegate.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/utils/ContextContainer.h>
#include "MountingOverrideDelegate.h"
namespace facebook::react {
using ShadowTreeCommitTransaction = std::function<RootShadowNode::Unshared(
const RootShadowNode& oldRootShadowNode)>;
/*
* Represents the shadow tree and its lifecycle.
*/
class ShadowTree final {
public:
using Unique = std::unique_ptr<ShadowTree>;
/*
* Represents a result of a `commit` operation.
*/
enum class CommitStatus {
Succeeded,
Failed,
Cancelled,
};
/*
* Represents commits' side-effects propagation mode.
*/
enum class CommitMode {
// Commits' side-effects are observable via `MountingCoordinator`.
// The rendering pipeline fully works end-to-end.
Normal,
// Commits' side-effects are *not* observable via `MountingCoordinator`.
// The mounting phase is skipped in the rendering pipeline.
Suspended,
};
struct CommitOptions {
// When set to true, Shadow Node state from current revision will be applied
// to the new revision. For more details see
// https://reactnative.dev/architecture/render-pipeline#react-native-renderer-state-updates
bool enableStateReconciliation{false};
// Indicates if mounting will be triggered synchronously and React will
// not get a chance to interrupt painting.
// This should be set to `false` when a commit is coming from React. It
// will then let React run layout effects and apply updates before paint.
// For all other commits, should be true.
bool mountSynchronously{true};
// Called during `tryCommit` phase. Returning true indicates current commit
// should yield to the next commit.
std::function<bool()> shouldYield;
};
/*
* Creates a new shadow tree instance.
*/
ShadowTree(
SurfaceId surfaceId,
const LayoutConstraints& layoutConstraints,
const LayoutContext& layoutContext,
const ShadowTreeDelegate& delegate,
const ContextContainer& contextContainer);
~ShadowTree();
/*
* Returns the `SurfaceId` associated with the shadow tree.
*/
SurfaceId getSurfaceId() const;
/*
* Sets and gets the commit mode.
* Changing commit mode from `Suspended` to `Normal` will flush all suspended
* changes to `MountingCoordinator`.
*/
void setCommitMode(CommitMode commitMode) const;
CommitMode getCommitMode() const;
/*
* Performs commit calling `transaction` function with a `oldRootShadowNode`
* and expecting a `newRootShadowNode` as a return value.
* The `transaction` function can cancel commit returning `nullptr`.
*/
CommitStatus tryCommit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const;
/*
* Calls `tryCommit` in a loop until it finishes successfully.
*/
CommitStatus commit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const;
/*
* Returns a `ShadowTreeRevision` representing the momentary state of
* the `ShadowTree`.
*/
ShadowTreeRevision getCurrentRevision() const;
/*
* Commit an empty tree (a new `RootShadowNode` with no children).
*/
void commitEmptyTree() const;
/**
* Forces the ShadowTree to ping its delegate that an update is available.
* Useful for animations on Android.
* @return
*/
void notifyDelegatesOfUpdates() const;
MountingCoordinator::Shared getMountingCoordinator() const;
private:
constexpr static ShadowTreeRevision::Number INITIAL_REVISION{0};
void mount(ShadowTreeRevision revision, bool mountSynchronously) const;
void emitLayoutEvents(
std::vector<const LayoutableShadowNode*>& affectedLayoutableNodes) const;
const SurfaceId surfaceId_;
const ShadowTreeDelegate& delegate_;
mutable std::shared_mutex commitMutex_;
mutable CommitMode commitMode_{
CommitMode::Normal}; // Protected by `commitMutex_`.
mutable ShadowTreeRevision currentRevision_; // Protected by `commitMutex_`.
mutable ShadowTreeRevision::Number
lastRevisionNumberWithNewState_; // Protected by `commitMutex_`.
MountingCoordinator::Shared mountingCoordinator_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/mounting/MountingCoordinator.h>
namespace facebook::react {
class ShadowTree;
/*
* Abstract class for ShadowTree's delegate.
*/
class ShadowTreeDelegate {
public:
/*
* Called right before a ShadowTree commits a new tree.
* The receiver can alter a new (proposed) shadow tree with another tree
* by returning the altered tree.
* Returning a `nullptr` cancels the commit.
*/
virtual RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree& shadowTree,
const RootShadowNode::Shared& oldRootShadowNode,
const RootShadowNode::Unshared& newRootShadowNode) const = 0;
/*
* Called right after Shadow Tree commit a new state of the tree.
*/
virtual void shadowTreeDidFinishTransaction(
MountingCoordinator::Shared mountingCoordinator,
bool mountSynchronously) const = 0;
virtual ~ShadowTreeDelegate() noexcept = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,67 @@
/*
* 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 "ShadowTreeRegistry.h"
#include <react/debug/react_native_assert.h>
namespace facebook::react {
ShadowTreeRegistry::~ShadowTreeRegistry() {
react_native_assert(
registry_.empty() && "Deallocation of non-empty `ShadowTreeRegistry`.");
}
void ShadowTreeRegistry::add(std::unique_ptr<ShadowTree>&& shadowTree) const {
std::unique_lock lock(mutex_);
registry_.emplace(shadowTree->getSurfaceId(), std::move(shadowTree));
}
std::unique_ptr<ShadowTree> ShadowTreeRegistry::remove(
SurfaceId surfaceId) const {
std::unique_lock lock(mutex_);
auto iterator = registry_.find(surfaceId);
if (iterator == registry_.end()) {
return {};
}
auto shadowTree = std::unique_ptr<ShadowTree>(iterator->second.release());
registry_.erase(iterator);
return shadowTree;
}
bool ShadowTreeRegistry::visit(
SurfaceId surfaceId,
const std::function<void(const ShadowTree& shadowTree)>& callback) const {
std::shared_lock lock(mutex_);
auto iterator = registry_.find(surfaceId);
if (iterator == registry_.end()) {
return false;
}
callback(*iterator->second);
return true;
}
void ShadowTreeRegistry::enumerate(
const std::function<void(const ShadowTree& shadowTree, bool& stop)>&
callback) const {
std::shared_lock lock(mutex_);
auto stop = false;
for (const auto& pair : registry_) {
callback(*pair.second, stop);
if (stop) {
return;
}
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,69 @@
/*
* 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.
*/
#pragma once
#include <shared_mutex>
#include <unordered_map>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/mounting/ShadowTree.h>
namespace facebook::react {
/*
* Owning registry of `ShadowTree`s.
*/
class ShadowTreeRegistry final {
public:
ShadowTreeRegistry() = default;
~ShadowTreeRegistry();
/*
* Adds a `ShadowTree` instance to the registry.
* The ownership of the instance is also transferred to the registry.
* Can be called from any thread.
*/
void add(std::unique_ptr<ShadowTree>&& shadowTree) const;
/*
* Removes a `ShadowTree` instance with given `surfaceId` from the registry
* and returns it as a result.
* The ownership of the instance is also transferred to the caller.
* Returns `nullptr` if a `ShadowTree` with given `surfaceId` was not found.
* Can be called from any thread.
*/
std::unique_ptr<ShadowTree> remove(SurfaceId surfaceId) const;
/*
* Finds a `ShadowTree` instance with a given `surfaceId` in the registry and
* synchronously calls the `callback` with a reference to the instance while
* the mutex is being acquired.
* Returns `true` if the registry has `ShadowTree` instance with corresponding
* `surfaceId`, otherwise returns `false` without calling the `callback`.
* Can be called from any thread.
*/
bool visit(
SurfaceId surfaceId,
const std::function<void(const ShadowTree& shadowTree)>& callback) const;
/*
* Enumerates all stored shadow trees.
* Set `stop` to `true` to interrupt the enumeration.
* Can be called from any thread.
*/
void enumerate(
const std::function<void(const ShadowTree& shadowTree, bool& stop)>&
callback) const;
private:
mutable std::shared_mutex mutex_;
mutable std::unordered_map<SurfaceId, std::unique_ptr<ShadowTree>>
registry_; // Protected by `mutex_`.
};
} // namespace facebook::react

View File

@@ -0,0 +1,8 @@
/*
* 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 "ShadowTreeRevision.h"

View File

@@ -0,0 +1,38 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/mounting/MountingOverrideDelegate.h>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
/*
* Represent a particular committed state of a shadow tree. The object contains
* a pointer to a root shadow node, a sequential number of commit and telemetry.
*/
class ShadowTreeRevision final {
public:
/*
* Sequential number of the commit that created this revision of a shadow
* tree.
*/
using Number = int64_t;
friend class ShadowTree;
friend class MountingCoordinator;
RootShadowNode::Shared rootShadowNode;
Number number;
TransactionTelemetry telemetry;
};
} // namespace facebook::react

View File

@@ -0,0 +1,98 @@
/*
* 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 "ShadowView.h"
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/LayoutableShadowNode.h>
namespace facebook::react {
static LayoutMetrics layoutMetricsFromShadowNode(const ShadowNode& shadowNode) {
auto layoutableShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&shadowNode);
return layoutableShadowNode != nullptr
? layoutableShadowNode->getLayoutMetrics()
: EmptyLayoutMetrics;
}
ShadowView::ShadowView(const ShadowNode& shadowNode)
: componentName(shadowNode.getComponentName()),
componentHandle(shadowNode.getComponentHandle()),
surfaceId(shadowNode.getSurfaceId()),
tag(shadowNode.getTag()),
traits(shadowNode.getTraits()),
props(shadowNode.getProps()),
eventEmitter(shadowNode.getEventEmitter()),
layoutMetrics(layoutMetricsFromShadowNode(shadowNode)),
state(shadowNode.getState()) {}
bool ShadowView::operator==(const ShadowView& rhs) const {
return std::tie(
this->surfaceId,
this->tag,
this->componentName,
this->props,
this->eventEmitter,
this->layoutMetrics,
this->state) ==
std::tie(
rhs.surfaceId,
rhs.tag,
rhs.componentName,
rhs.props,
rhs.eventEmitter,
rhs.layoutMetrics,
rhs.state);
}
bool ShadowView::operator!=(const ShadowView& rhs) const {
return !(*this == rhs);
}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowView& object) {
return object.componentHandle == 0 ? "Invalid" : object.componentName;
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowView& object,
DebugStringConvertibleOptions options) {
return {
{"surfaceId", getDebugDescription(object.surfaceId, options)},
{"tag", getDebugDescription(object.tag, options)},
{"traits", getDebugDescription(object.traits, options)},
{"componentName", object.componentName},
{"props", getDebugDescription(object.props, options)},
{"eventEmitter", getDebugDescription(object.eventEmitter, options)},
{"layoutMetrics", getDebugDescription(object.layoutMetrics, options)},
{"state", getDebugDescription(object.state, options)},
};
}
#endif
bool ShadowViewNodePair::operator==(const ShadowViewNodePair& rhs) const {
return this->shadowNode == rhs.shadowNode;
}
bool ShadowViewNodePair::operator!=(const ShadowViewNodePair& rhs) const {
return !(*this == rhs);
}
bool ShadowViewNodePairLegacy::operator==(
const ShadowViewNodePairLegacy& rhs) const {
return this->shadowNode == rhs.shadowNode;
}
bool ShadowViewNodePairLegacy::operator!=(
const ShadowViewNodePairLegacy& rhs) const {
return !(*this == rhs);
}
} // namespace facebook::react

View File

@@ -0,0 +1,139 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/EventEmitter.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/Props.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/debug/flags.h>
#include <react/utils/hash_combine.h>
namespace facebook::react {
/*
* Describes a view that can be mounted.
* This is exposed to the mounting layer.
*/
struct ShadowView final {
ShadowView() = default;
ShadowView(const ShadowView& shadowView) = default;
ShadowView(ShadowView&& shadowView) noexcept = default;
/*
* Constructs a `ShadowView` from given `ShadowNode`.
*/
explicit ShadowView(const ShadowNode& shadowNode);
ShadowView& operator=(const ShadowView& other) = default;
ShadowView& operator=(ShadowView&& other) = default;
bool operator==(const ShadowView& rhs) const;
bool operator!=(const ShadowView& rhs) const;
ComponentName componentName{};
ComponentHandle componentHandle{};
SurfaceId surfaceId{};
Tag tag{};
ShadowNodeTraits traits{};
Props::Shared props{};
EventEmitter::Shared eventEmitter{};
LayoutMetrics layoutMetrics{EmptyLayoutMetrics};
State::Shared state{};
};
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowView& object);
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowView& object,
DebugStringConvertibleOptions options);
#endif
/*
* Describes pair of a `ShadowView` and a `ShadowNode`.
* This is not exposed to the mounting layer.
*
*/
struct ShadowViewNodePair final {
using NonOwningList = std::vector<ShadowViewNodePair*>;
using OwningList = std::vector<ShadowViewNodePair>;
ShadowView shadowView;
const ShadowNode* shadowNode;
bool flattened{false};
bool isConcreteView{true};
Point contextOrigin{0, 0};
size_t mountIndex{0};
/**
* This is nullptr unless `inOtherTree` is set to true.
* We rely on this only for marginal cases. TODO: could we
* rely on this more heavily to simplify the diffing algorithm
* overall?
*/
mutable const ShadowViewNodePair* otherTreePair{nullptr};
/*
* The stored pointer to `ShadowNode` represents an identity of the pair.
*/
bool operator==(const ShadowViewNodePair& rhs) const;
bool operator!=(const ShadowViewNodePair& rhs) const;
bool inOtherTree() const {
return this->otherTreePair != nullptr;
}
};
/*
* Describes pair of a `ShadowView` and a `ShadowNode`.
* This is not exposed to the mounting layer.
*
*/
struct ShadowViewNodePairLegacy final {
using OwningList = std::vector<ShadowViewNodePairLegacy>;
ShadowView shadowView;
const ShadowNode* shadowNode;
bool flattened{false};
bool isConcreteView{true};
size_t mountIndex{0};
bool inOtherTree{false};
/*
* The stored pointer to `ShadowNode` represents an identity of the pair.
*/
bool operator==(const ShadowViewNodePairLegacy& rhs) const;
bool operator!=(const ShadowViewNodePairLegacy& rhs) const;
};
} // namespace facebook::react
namespace std {
template <>
struct hash<facebook::react::ShadowView> {
size_t operator()(const facebook::react::ShadowView& shadowView) const {
return facebook::react::hash_combine(
0,
shadowView.surfaceId,
shadowView.componentHandle,
shadowView.tag,
shadowView.props,
shadowView.eventEmitter,
shadowView.layoutMetrics,
shadowView.state);
}
};
} // namespace std

View File

@@ -0,0 +1,178 @@
/*
* 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 "ShadowViewMutation.h"
#include <utility>
namespace facebook::react {
/**
* Initialize static feature flags for this module.
* These flags should be treated as temporary.
*/
bool ShadowViewMutation::PlatformSupportsRemoveDeleteTreeInstruction = false;
ShadowViewMutation ShadowViewMutation::CreateMutation(ShadowView shadowView) {
return {
/* .type = */ Create,
/* .parentShadowView = */ {},
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(shadowView),
/* .index = */ -1,
};
}
ShadowViewMutation ShadowViewMutation::DeleteMutation(
ShadowView shadowView,
bool isRedundantOperation) {
return {
/* .type = */ Delete,
/* .parentShadowView = */ {},
/* .oldChildShadowView = */ std::move(shadowView),
/* .newChildShadowView = */ {},
/* .index = */ -1,
/* .isRedundantOperation */ isRedundantOperation,
};
}
ShadowViewMutation ShadowViewMutation::InsertMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index) {
return {
/* .type = */ Insert,
/* .parentShadowView = */ std::move(parentShadowView),
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(childShadowView),
/* .index = */ index,
};
}
ShadowViewMutation ShadowViewMutation::RemoveMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index,
bool isRedundantOperation) {
return {
/* .type = */ Remove,
/* .parentShadowView = */ std::move(parentShadowView),
/* .oldChildShadowView = */ std::move(childShadowView),
/* .newChildShadowView = */ {},
/* .index = */ index,
/* .isRedundantOperation */ isRedundantOperation,
};
}
ShadowViewMutation ShadowViewMutation::RemoveDeleteTreeMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index) {
return {
/* .type = */ RemoveDeleteTree,
/* .parentShadowView = */ std::move(parentShadowView),
/* .oldChildShadowView = */ std::move(childShadowView),
/* .newChildShadowView = */ {},
/* .index = */ index,
};
}
ShadowViewMutation ShadowViewMutation::UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
ShadowView parentShadowView) {
return {
/* .type = */ Update,
/* .parentShadowView = */ std::move(parentShadowView),
/* .oldChildShadowView = */ std::move(oldChildShadowView),
/* .newChildShadowView = */ std::move(newChildShadowView),
/* .index = */ -1,
};
}
bool ShadowViewMutation::mutatedViewIsVirtual() const {
bool viewIsVirtual = false;
#ifdef ANDROID
// Explanation: Even for non-virtual views,
// for "Insert" mutations, oldChildShadowView is always empty.
// for "Remove" mutations, newChildShadowView is always empty.
// Thus, to see if a view is virtual, we need to always check both the old and
// new View.
viewIsVirtual = newChildShadowView.layoutMetrics == EmptyLayoutMetrics &&
oldChildShadowView.layoutMetrics == EmptyLayoutMetrics;
#endif
return viewIsVirtual;
}
ShadowViewMutation::ShadowViewMutation(
Type type,
ShadowView parentShadowView,
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
int index,
bool isRedundantOperation)
: type(type),
parentShadowView(std::move(parentShadowView)),
oldChildShadowView(std::move(oldChildShadowView)),
newChildShadowView(std::move(newChildShadowView)),
index(index),
isRedundantOperation(isRedundantOperation) {}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowViewMutation& mutation) {
switch (mutation.type) {
case ShadowViewMutation::Create:
return "Create";
case ShadowViewMutation::Delete:
return "Delete";
case ShadowViewMutation::Insert:
return "Insert";
case ShadowViewMutation::Remove:
return "Remove";
case ShadowViewMutation::Update:
return "Update";
case ShadowViewMutation::RemoveDeleteTree:
return "RemoveDeleteTree";
}
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowViewMutation& mutation,
DebugStringConvertibleOptions options) {
return {
mutation.oldChildShadowView.componentHandle != 0
? DebugStringConvertibleObject{"oldChild",
getDebugDescription(
mutation.oldChildShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.newChildShadowView.componentHandle != 0
? DebugStringConvertibleObject{"newChild",
getDebugDescription(
mutation.newChildShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.parentShadowView.componentHandle != 0
? DebugStringConvertibleObject{"parent",
getDebugDescription(
mutation.parentShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.index != -1
? DebugStringConvertibleObject{"index",
getDebugDescription(
mutation.index, options)}
: DebugStringConvertibleObject{},
};
}
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,134 @@
/*
* 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.
*/
#pragma once
#include <vector>
#include <react/renderer/mounting/ShadowView.h>
namespace facebook::react {
/*
* Describes a single native view tree mutation which may contain
* pointers to an old shadow view, a new shadow view, a parent shadow view and
* final index of inserted or updated view.
* Use static methods to instantiate mutations of different types.
*/
struct ShadowViewMutation final {
using List = std::vector<ShadowViewMutation>;
ShadowViewMutation() = delete;
#pragma mark - Platform feature flags
static bool PlatformSupportsRemoveDeleteTreeInstruction;
#pragma mark - Designated Initializers
/*
* Creates and returns an `Create` mutation.
*/
static ShadowViewMutation CreateMutation(ShadowView shadowView);
/*
* Creates and returns an `Delete` mutation.
*/
static ShadowViewMutation DeleteMutation(
ShadowView shadowView,
bool isRedundantOperation = false);
/*
* Creates and returns an `Insert` mutation.
*/
static ShadowViewMutation InsertMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index);
/*
* Creates and returns a `Remove` mutation.
*/
static ShadowViewMutation RemoveMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index,
bool isRedundantOperation = false);
/*
* Creates and returns a `RemoveDelete` mutation.
* This is a signal to (for supported platforms)
* remove and delete an entire subtree with a single
* instruction.
*/
static ShadowViewMutation RemoveDeleteTreeMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index);
/*
* Creates and returns an `Update` mutation.
*/
static ShadowViewMutation UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
ShadowView parentShadowView);
#pragma mark - Type
enum Type {
Create = 1,
Delete = 2,
Insert = 4,
Remove = 8,
Update = 16,
RemoveDeleteTree = 32
};
#pragma mark - Fields
Type type = {Create};
ShadowView parentShadowView = {};
ShadowView oldChildShadowView = {};
ShadowView newChildShadowView = {};
int index = -1;
// RemoveDeleteTree causes many Remove/Delete operations to be redundant.
// However, we must internally produce all of them for any consumers that
// rely on explicit instructions to remove/delete every node in the tree.
// Notably (as of the time of writing this) LayoutAnimations.
bool isRedundantOperation = false;
// Some platforms can have the notion of virtual views - views that are in the
// ShadowTree hierarchy but never are on the platform. Generally this is used
// so notify the platform that a view exists so that we can keep EventEmitters
// around, to notify JS of something. This mechanism is DEPRECATED and it is
// highly recommended that you NOT make use of this in your platform!
bool mutatedViewIsVirtual() const;
private:
ShadowViewMutation(
Type type,
ShadowView parentShadowView,
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
int index,
bool isRedundantOperation = false);
};
using ShadowViewMutationList = std::vector<ShadowViewMutation>;
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowViewMutation& mutation);
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowViewMutation& mutation,
DebugStringConvertibleOptions options);
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,97 @@
/*
* 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 "StubView.h"
#ifdef STUB_VIEW_TREE_VERBOSE
#include <glog/logging.h>
#endif
namespace facebook::react {
StubView::operator ShadowView() const {
auto shadowView = ShadowView{};
shadowView.componentName = componentName;
shadowView.componentHandle = componentHandle;
shadowView.surfaceId = surfaceId;
shadowView.tag = tag;
shadowView.props = props;
shadowView.eventEmitter = eventEmitter;
shadowView.layoutMetrics = layoutMetrics;
shadowView.state = state;
return shadowView;
}
void StubView::update(const ShadowView& shadowView) {
componentName = shadowView.componentName;
componentHandle = shadowView.componentHandle;
surfaceId = shadowView.surfaceId;
tag = shadowView.tag;
props = shadowView.props;
eventEmitter = shadowView.eventEmitter;
layoutMetrics = shadowView.layoutMetrics;
state = shadowView.state;
}
bool operator==(const StubView& lhs, const StubView& rhs) {
if (lhs.props != rhs.props) {
#ifdef STUB_VIEW_TREE_VERBOSE
LOG(ERROR) << "StubView: props do not match. lhs hash: "
<< std::hash<ShadowView>{}((ShadowView)lhs)
<< " rhs hash: " << std::hash<ShadowView>{}((ShadowView)rhs);
#endif
return false;
}
if (lhs.layoutMetrics != rhs.layoutMetrics) {
#ifdef STUB_VIEW_TREE_VERBOSE
LOG(ERROR) << "StubView: layoutMetrics do not match lhs hash: "
<< std::hash<ShadowView>{}((ShadowView)lhs)
<< " rhs hash: " << std::hash<ShadowView>{}((ShadowView)rhs);
#endif
return false;
}
return true;
}
bool operator!=(const StubView& lhs, const StubView& rhs) {
return !(lhs == rhs);
}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const StubView& stubView) {
return std::string{"Stub"} +
std::string{
stubView.componentHandle != 0 ? stubView.componentName : "[invalid]"};
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const StubView& stubView,
DebugStringConvertibleOptions options) {
return {
{"surfaceId", getDebugDescription(stubView.surfaceId, options)},
{"tag", getDebugDescription(stubView.tag, options)},
{"props", getDebugDescription(stubView.props, options)},
{"eventEmitter", getDebugDescription(stubView.eventEmitter, options)},
{"layoutMetrics", getDebugDescription(stubView.layoutMetrics, options)},
{"state", getDebugDescription(stubView.state, options)},
};
}
std::vector<StubView> getDebugChildren(
const StubView& stubView,
DebugStringConvertibleOptions /*options*/) {
std::vector<StubView> result;
for (const auto& child : stubView.children) {
result.push_back(*child);
}
return result;
}
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,61 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <vector>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/State.h>
#include <react/renderer/debug/debugStringConvertibleUtils.h>
#include <react/renderer/mounting/ShadowView.h>
namespace facebook::react {
static const int NO_VIEW_TAG = -1;
class StubView final {
public:
using Shared = std::shared_ptr<StubView>;
StubView() = default;
StubView(const StubView& stubView) = default;
operator ShadowView() const;
void update(const ShadowView& shadowView);
ComponentName componentName;
ComponentHandle componentHandle;
SurfaceId surfaceId;
Tag tag;
Props::Shared props;
SharedEventEmitter eventEmitter;
LayoutMetrics layoutMetrics;
State::Shared state;
std::vector<StubView::Shared> children;
Tag parentTag{NO_VIEW_TAG};
};
bool operator==(const StubView& lhs, const StubView& rhs);
bool operator!=(const StubView& lhs, const StubView& rhs);
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const StubView& stubView);
std::vector<DebugStringConvertibleObject> getDebugProps(
const StubView& stubView,
DebugStringConvertibleOptions options);
std::vector<StubView> getDebugChildren(
const StubView& stubView,
DebugStringConvertibleOptions options);
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,313 @@
/*
* 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 "StubViewTree.h"
#include <glog/logging.h>
#include <react/debug/react_native_assert.h>
#ifdef STUB_VIEW_TREE_VERBOSE
#define STUB_VIEW_LOG(code) code
#else
#define STUB_VIEW_LOG(code)
#endif
namespace facebook::react {
StubViewTree::StubViewTree(const ShadowView& shadowView) {
auto view = std::make_shared<StubView>();
view->update(shadowView);
rootTag_ = shadowView.tag;
registry_[shadowView.tag] = view;
}
const StubView& StubViewTree::getRootStubView() const {
return *registry_.at(rootTag_);
}
const StubView& StubViewTree::getStubView(Tag tag) const {
return *registry_.at(tag);
}
size_t StubViewTree::size() const {
return registry_.size();
}
void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
STUB_VIEW_LOG({ LOG(ERROR) << "StubView: Mutating Begin"; });
for (const auto& mutation : mutations) {
switch (mutation.type) {
case ShadowViewMutation::Create: {
react_native_assert(mutation.parentShadowView == ShadowView{});
react_native_assert(mutation.oldChildShadowView == ShadowView{});
react_native_assert(mutation.newChildShadowView.props);
auto stubView = std::make_shared<StubView>();
stubView->update(mutation.newChildShadowView);
auto tag = mutation.newChildShadowView.tag;
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Create [" << tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)*stubView);
});
if (hasTag(tag)) {
LOG(ERROR) << "StubView: Create [" << tag << "]: tag already exists"
<< (tag == rootTag_ ? " (and it's the root tag)" : "")
<< ". The current registry: ";
dumpTags(LOG(ERROR));
}
react_native_assert(!hasTag(tag));
registry_[tag] = stubView;
break;
}
case ShadowViewMutation::Delete: {
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Delete [" << mutation.oldChildShadowView.tag
<< "] ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
});
react_native_assert(mutation.parentShadowView == ShadowView{});
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto tag = mutation.oldChildShadowView.tag;
react_native_assert(hasTag(tag));
auto stubView = registry_[tag];
if ((ShadowView)(*stubView) != mutation.oldChildShadowView) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: DELETE mutation assertion failure: oldChildShadowView does not match stubView: ["
<< mutation.oldChildShadowView.tag << "] stub hash: ##"
<< std::hash<ShadowView>{}((ShadowView)*stubView)
<< " old mutation hash: ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "StubView: "
<< getDebugPropsDescription((ShadowView)*stubView, {});
LOG(ERROR) << "OldChildShadowView: "
<< getDebugPropsDescription(
mutation.oldChildShadowView, {});
#endif
}
react_native_assert(
(ShadowView)(*stubView) == mutation.oldChildShadowView);
registry_.erase(tag);
break;
}
case ShadowViewMutation::Insert: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.oldChildShadowView == ShadowView{});
auto parentTag = mutation.parentShadowView.tag;
auto childTag = mutation.newChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: INSERT mutation assertion failure: parentTag not found: ["
<< parentTag << "] inserting child: [" << childTag << "]";
}
if (!hasTag(childTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: INSERT mutation assertion failure: childTag not found: ["
<< parentTag << "] inserting child: [" << childTag << "]";
}
react_native_assert(hasTag(parentTag));
auto parentStubView = registry_[parentTag];
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
childStubView->update(mutation.newChildShadowView);
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Insert [" << childTag << "] into ["
<< parentTag << "] @" << mutation.index << "("
<< parentStubView->children.size() << " children)";
});
react_native_assert(childStubView->parentTag == NO_VIEW_TAG);
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >=
static_cast<size_t>(mutation.index));
childStubView->parentTag = parentTag;
parentStubView->children.insert(
parentStubView->children.begin() + mutation.index, childStubView);
} else {
auto childTag = mutation.newChildShadowView.tag;
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
childStubView->update(mutation.newChildShadowView);
}
break;
}
case ShadowViewMutation::Remove: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto parentTag = mutation.parentShadowView.tag;
auto childTag = mutation.oldChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: REMOVE mutation assertion failure: parentTag not found: ["
<< parentTag << "] removing child: [" << childTag << "]";
}
react_native_assert(hasTag(parentTag));
auto parentStubView = registry_[parentTag];
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Remove [" << childTag << "] from ["
<< parentTag << "] @" << mutation.index << " with "
<< parentStubView->children.size() << " children";
});
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >
static_cast<size_t>(mutation.index));
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
if ((ShadowView)(*childStubView) != mutation.oldChildShadowView) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: REMOVE mutation assertion failure: oldChildShadowView does not match oldStubView: ["
<< mutation.oldChildShadowView.tag << "] stub hash: ##"
<< std::hash<ShadowView>{}((ShadowView)*childStubView)
<< " old mutation hash: ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "ChildStubView: "
<< getDebugPropsDescription(
(ShadowView)*childStubView, {});
LOG(ERROR) << "OldChildShadowView: "
<< getDebugPropsDescription(
mutation.oldChildShadowView, {});
#endif
}
react_native_assert(
(ShadowView)(*childStubView) == mutation.oldChildShadowView);
react_native_assert(childStubView->parentTag == parentTag);
STUB_VIEW_LOG({
std::string strChildList = "";
int i = 0;
for (auto const& child : parentStubView->children) {
strChildList.append(std::to_string(i));
strChildList.append(":");
strChildList.append(std::to_string(child->tag));
strChildList.append(", ");
i++;
}
LOG(ERROR) << "StubView: BEFORE REMOVE: Children of " << parentTag
<< ": " << strChildList;
});
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >
static_cast<size_t>(mutation.index) &&
parentStubView->children[mutation.index]->tag ==
childStubView->tag);
childStubView->parentTag = NO_VIEW_TAG;
parentStubView->children.erase(
parentStubView->children.begin() + mutation.index);
}
break;
}
case ShadowViewMutation::RemoveDeleteTree: {
// TODO: do something here
break;
}
case ShadowViewMutation::Update: {
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Update [" << mutation.newChildShadowView.tag
<< "] old hash: ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView)
<< " new hash: ##"
<< std::hash<ShadowView>{}(mutation.newChildShadowView);
});
react_native_assert(mutation.oldChildShadowView.tag != 0);
react_native_assert(mutation.newChildShadowView.tag != 0);
react_native_assert(mutation.newChildShadowView.props);
react_native_assert(
mutation.newChildShadowView.tag == mutation.oldChildShadowView.tag);
react_native_assert(hasTag(mutation.newChildShadowView.tag));
auto oldStubView = registry_[mutation.newChildShadowView.tag];
react_native_assert(oldStubView->tag != 0);
if ((ShadowView)(*oldStubView) != mutation.oldChildShadowView) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: UPDATE mutation assertion failure: oldChildShadowView does not match oldStubView: ["
<< mutation.oldChildShadowView.tag << "] old stub hash: ##"
<< std::hash<ShadowView>{}((ShadowView)*oldStubView)
<< " old mutation hash: ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "OldStubView: "
<< getDebugPropsDescription((ShadowView)*oldStubView, {});
LOG(ERROR) << "OldChildShadowView: "
<< getDebugPropsDescription(
mutation.oldChildShadowView, {});
#endif
}
react_native_assert(
(ShadowView)(*oldStubView) == mutation.oldChildShadowView);
oldStubView->update(mutation.newChildShadowView);
// Hash for stub view and the ShadowView should be identical - this
// tests that StubView and ShadowView hash are equivalent.
react_native_assert(
std::hash<ShadowView>{}((ShadowView)*oldStubView) ==
std::hash<ShadowView>{}(mutation.newChildShadowView));
break;
}
}
}
STUB_VIEW_LOG({ LOG(ERROR) << "StubView: Mutating End"; });
// For iOS especially: flush logs because some might be lost on iOS if an
// assert is hit right after this.
google::FlushLogFiles(google::GLOG_INFO);
}
std::ostream& StubViewTree::dumpTags(std::ostream& stream) {
for (const auto& pair : registry_) {
auto& stubView = *registry_.at(pair.first);
stream << "[" << stubView.tag << "]##"
<< std::hash<ShadowView>{}((ShadowView)stubView) << " ";
}
return stream;
}
bool operator==(const StubViewTree& lhs, const StubViewTree& rhs) {
if (lhs.registry_.size() != rhs.registry_.size()) {
STUB_VIEW_LOG({
LOG(ERROR) << "Registry sizes are different. Sizes: LHS: "
<< lhs.registry_.size() << " RHS: " << rhs.registry_.size();
LOG(ERROR) << "Tags in LHS: ";
lhs.dumpTagsHash(LOG(ERROR));
LOG(ERROR) << "Tags in RHS: ";
rhs.dumpTagsHash(LOG(ERROR));
});
return false;
}
for (const auto& pair : lhs.registry_) {
auto& lhsStubView = *lhs.registry_.at(pair.first);
auto& rhsStubView = *rhs.registry_.at(pair.first);
if (lhsStubView != rhsStubView) {
STUB_VIEW_LOG({
LOG(ERROR) << "Registry entries are different. LHS: ["
<< lhsStubView.tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)lhsStubView)
<< " RHS: [" << rhsStubView.tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)rhsStubView);
});
return false;
}
}
return true;
}
bool operator!=(const StubViewTree& lhs, const StubViewTree& rhs) {
return !(lhs == rhs);
}
} // namespace facebook::react

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <unordered_map>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/StubView.h>
namespace facebook::react {
class StubViewTree {
public:
StubViewTree() = default;
StubViewTree(const ShadowView& shadowView);
void mutate(const ShadowViewMutationList& mutations);
const StubView& getRootStubView() const;
/*
* Returns a view with given tag.
*/
const StubView& getStubView(Tag tag) const;
/*
* Returns the total amount of views in the tree.
*/
size_t size() const;
private:
Tag rootTag_{};
std::unordered_map<Tag, StubView::Shared> registry_{};
friend bool operator==(const StubViewTree& lhs, const StubViewTree& rhs);
friend bool operator!=(const StubViewTree& lhs, const StubViewTree& rhs);
std::ostream& dumpTags(std::ostream& stream);
bool hasTag(Tag tag) const {
return registry_.find(tag) != registry_.end();
}
};
bool operator==(const StubViewTree& lhs, const StubViewTree& rhs);
bool operator!=(const StubViewTree& lhs, const StubViewTree& rhs);
} // namespace facebook::react

View File

@@ -0,0 +1,53 @@
/*
* 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 "TelemetryController.h"
#include <react/renderer/mounting/MountingCoordinator.h>
namespace facebook::react {
TelemetryController::TelemetryController(
const MountingCoordinator& mountingCoordinator) noexcept
: mountingCoordinator_(mountingCoordinator) {}
bool TelemetryController::pullTransaction(
const MountingTransactionCallback& willMount,
const MountingTransactionCallback& doMount,
const MountingTransactionCallback& didMount) const {
auto optional = mountingCoordinator_.pullTransaction();
if (!optional.has_value()) {
return false;
}
auto transaction = std::move(*optional);
auto& telemetry = transaction.getTelemetry();
auto numberOfMutations = static_cast<int>(transaction.getMutations().size());
mutex_.lock();
auto compoundTelemetry = compoundTelemetry_;
mutex_.unlock();
willMount(transaction, compoundTelemetry);
telemetry.willMount();
doMount(transaction, compoundTelemetry);
telemetry.didMount();
compoundTelemetry.incorporate(telemetry, numberOfMutations);
didMount(transaction, compoundTelemetry);
mutex_.lock();
compoundTelemetry_ = compoundTelemetry;
mutex_.unlock();
return true;
}
} // namespace facebook::react

View File

@@ -0,0 +1,58 @@
/*
* 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.
*/
#pragma once
#include <functional>
#include <mutex>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
class MountingCoordinator;
using MountingTransactionCallback = std::function<void(
const MountingTransaction& transaction,
const SurfaceTelemetry& surfaceTelemetry)>;
/*
* Provides convenient tools for aggregating and accessing telemetry data
* associated with running Surface.
*/
class TelemetryController final {
friend class MountingCoordinator;
/*
* To be used by `MountingCoordinator`.
*/
TelemetryController(const MountingCoordinator& mountingCoordinator) noexcept;
/*
* Not copyable.
*/
TelemetryController(const TelemetryController& other) noexcept = delete;
TelemetryController& operator=(const TelemetryController& other) noexcept =
delete;
public:
/*
* Calls `MountingCoordinator::pullTransaction()` and aggregates telemetry.
*/
bool pullTransaction(
const MountingTransactionCallback& willMount,
const MountingTransactionCallback& doMount,
const MountingTransactionCallback& didMount) const;
private:
const MountingCoordinator& mountingCoordinator_;
mutable SurfaceTelemetry compoundTelemetry_{};
mutable std::mutex mutex_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,98 @@
/*
* 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 "stubs.h"
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/core/ShadowNodeFragment.h>
#include <react/renderer/mounting/Differentiator.h>
namespace facebook::react {
/*
* Sorting comparator for `reorderInPlaceIfNeeded`.
*/
static bool shouldFirstPairComesBeforeSecondOne(
const ShadowViewNodePair& lhs,
const ShadowViewNodePair& rhs) noexcept {
return lhs.shadowNode->getOrderIndex() < rhs.shadowNode->getOrderIndex();
}
/*
* Reorders pairs in-place based on `orderIndex` using a stable sort algorithm.
*/
static void reorderInPlaceIfNeeded(
ShadowViewNodePair::OwningList& pairs) noexcept {
// This is a simplified version of the function intentionally copied from
// `Differentiator.cpp`.
std::stable_sort(
pairs.begin(), pairs.end(), &shouldFirstPairComesBeforeSecondOne);
}
/*
* Generates `create` and `insert` instructions recursively traversing a shadow
* tree.
* This is a trivial implementation of diffing algorithm that can only "diff"
* an empty tree with some other one.
*/
static void calculateShadowViewMutationsForNewTree(
ShadowViewMutation::List& mutations,
const ShadowView& parentShadowView,
ShadowViewNodePair::OwningList newChildPairs) {
// Sorting pairs based on `orderIndex` if needed.
reorderInPlaceIfNeeded(newChildPairs);
for (size_t index = 0; index < newChildPairs.size(); index++) {
const auto& newChildPair = newChildPairs[index];
mutations.push_back(
ShadowViewMutation::CreateMutation(newChildPair.shadowView));
mutations.push_back(ShadowViewMutation::InsertMutation(
parentShadowView, newChildPair.shadowView, static_cast<int>(index)));
auto newGrandChildPairs =
sliceChildShadowNodeViewPairsForTesting(*newChildPair.shadowNode);
calculateShadowViewMutationsForNewTree(
mutations, newChildPair.shadowView, newGrandChildPairs);
}
}
StubViewTree buildStubViewTreeWithoutUsingDifferentiator(
const ShadowNode& rootShadowNode) {
auto mutations = ShadowViewMutation::List{};
mutations.reserve(256);
calculateShadowViewMutationsForNewTree(
mutations,
ShadowView(rootShadowNode),
sliceChildShadowNodeViewPairsForTesting(rootShadowNode));
auto emptyRootShadowNode = rootShadowNode.clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
ShadowNode::emptySharedShadowNodeSharedList()});
auto stubViewTree = StubViewTree(ShadowView(*emptyRootShadowNode));
stubViewTree.mutate(mutations);
return stubViewTree;
}
StubViewTree buildStubViewTreeUsingDifferentiator(
const ShadowNode& rootShadowNode) {
auto emptyRootShadowNode = rootShadowNode.clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
ShadowNode::emptySharedShadowNodeSharedList()});
auto mutations =
calculateShadowViewMutations(*emptyRootShadowNode, rootShadowNode);
auto stubViewTree = StubViewTree(ShadowView(*emptyRootShadowNode));
stubViewTree.mutate(mutations);
return stubViewTree;
}
} // namespace facebook::react

View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include "StubView.h"
#include "StubViewTree.h"
namespace facebook::react {
/*
* Builds a ShadowView tree from given root ShadowNode using custom built-in
* implementation (*without* using Differentiator).
*/
StubViewTree buildStubViewTreeWithoutUsingDifferentiator(
const ShadowNode& rootShadowNode);
/*
* Builds a ShadowView tree from given root ShadowNode using Differentiator by
* generating mutation instructions between empty and final trees.
*/
StubViewTree buildStubViewTreeUsingDifferentiator(
const ShadowNode& rootShadowNode);
} // namespace facebook::react

View File

@@ -0,0 +1,717 @@
/*
* 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 <memory>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/stubs.h>
#include <react/test_utils/shadowTreeGeneration.h>
#include <glog/logging.h>
#include <gtest/gtest.h>
namespace facebook::react {
static SharedViewProps nonFlattenedDefaultProps(
const ComponentDescriptor& componentDescriptor) {
folly::dynamic dynamic = folly::dynamic::object();
dynamic["position"] = "absolute";
dynamic["top"] = 0;
dynamic["left"] = 0;
dynamic["width"] = 100;
dynamic["height"] = 100;
dynamic["nativeId"] = "NativeId";
dynamic["accessible"] = true;
ContextContainer contextContainer;
contextContainer.insert(
"ReactNativeConfig", std::make_shared<EmptyReactNativeConfig>());
PropsParserContext parserContext{-1, contextContainer};
return std::static_pointer_cast<const ViewProps>(
componentDescriptor.cloneProps(
parserContext, nullptr, RawProps{dynamic}));
}
static ShadowNode::Shared makeNode(
const ComponentDescriptor& componentDescriptor,
int tag,
const ShadowNode::ListOfShared& children,
bool flattened = false) {
auto props = flattened ? generateDefaultProps(componentDescriptor)
: nonFlattenedDefaultProps(componentDescriptor);
return componentDescriptor.createShadowNode(
ShadowNodeFragment{
props, std::make_shared<ShadowNode::ListOfShared>(children)},
componentDescriptor.createFamily({tag, SurfaceId(1), nullptr}));
}
/**
* Test reordering of views with the same parent:
*
* For instance:
* A -> [B,C,D] ==> A -> [D,B,C]
*
* In the V1 of diffing this would produce 3 removes and 3 inserts, but with
* some cleverness we can reduce this to 1 remove and 1 insert.
*/
TEST(MountingTest, testReorderingInstructionGeneration) {
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
contextContainer->insert(
"ReactNativeConfig", std::make_shared<EmptyReactNativeConfig>());
auto componentDescriptorParameters =
ComponentDescriptorParameters{eventDispatcher, contextContainer, nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
auto rootFamily =
rootComponentDescriptor.createFamily({Tag(1), SurfaceId(1), nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{RootShadowNode::defaultSharedProps()},
rootFamily)));
PropsParserContext parserContext{-1, *contextContainer};
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
Size{512, 0}, Size{512, std::numeric_limits<Float>::infinity()}},
LayoutContext{});
auto childA = makeNode(viewComponentDescriptor, 100, {});
auto childB = makeNode(viewComponentDescriptor, 101, {});
auto childC = makeNode(viewComponentDescriptor, 102, {});
auto childD = makeNode(viewComponentDescriptor, 103, {});
auto childE = makeNode(viewComponentDescriptor, 104, {});
auto childF = makeNode(viewComponentDescriptor, 105, {});
auto childG = makeNode(viewComponentDescriptor, 106, {});
auto childH = makeNode(viewComponentDescriptor, 107, {});
auto childI = makeNode(viewComponentDescriptor, 108, {});
auto childJ = makeNode(viewComponentDescriptor, 109, {});
auto childK = makeNode(viewComponentDescriptor, 110, {});
auto family =
viewComponentDescriptor.createFamily({10, SurfaceId(1), nullptr});
// Construct "identical" shadow nodes: they differ only in children.
auto shadowNodeV1 = viewComponentDescriptor.createShadowNode(
ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childC, childD})},
family);
auto shadowNodeV2 = shadowNodeV1->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childA, childB, childC, childD})});
auto shadowNodeV3 = shadowNodeV2->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childC, childD})});
auto shadowNodeV4 = shadowNodeV3->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childD, childE})});
auto shadowNodeV5 = shadowNodeV4->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childA, childE, childC})});
auto shadowNodeV6 = shadowNodeV5->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childB, childA, childD, childF, childE, childC})});
auto shadowNodeV7 = shadowNodeV6->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childF,
childE,
childC,
childD,
childG,
childH,
childI,
childJ,
childK})});
// Injecting a tree into the root node.
auto rootNodeV1 = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV1})}));
auto rootNodeV2 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV1->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV2})}));
auto rootNodeV3 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV2->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV3})}));
auto rootNodeV4 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV3->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV4})}));
auto rootNodeV5 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV4->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV5})}));
auto rootNodeV6 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV5->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV6})}));
auto rootNodeV7 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV6->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV7})}));
// Layout
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV1{};
affectedLayoutableNodesV1.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV1)
->layoutIfNeeded(&affectedLayoutableNodesV1);
rootNodeV1->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV2{};
affectedLayoutableNodesV2.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV2)
->layoutIfNeeded(&affectedLayoutableNodesV2);
rootNodeV2->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV3{};
affectedLayoutableNodesV3.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV3)
->layoutIfNeeded(&affectedLayoutableNodesV3);
rootNodeV3->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV4{};
affectedLayoutableNodesV4.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV4)
->layoutIfNeeded(&affectedLayoutableNodesV4);
rootNodeV4->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV5{};
affectedLayoutableNodesV5.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV5)
->layoutIfNeeded(&affectedLayoutableNodesV5);
rootNodeV5->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV6{};
affectedLayoutableNodesV6.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV6)
->layoutIfNeeded(&affectedLayoutableNodesV6);
rootNodeV6->sealRecursive();
// This block displays all the mutations for debugging purposes.
/*
LOG(ERROR) << "Num mutations: " << mutations.size();
for (auto const &mutation : mutations) {
switch (mutation.type) {
case ShadowViewMutation::Create: {
LOG(ERROR) << "CREATE " << mutation.newChildShadowView.tag;
break;
}
case ShadowViewMutation::Delete: {
LOG(ERROR) << "DELETE " << mutation.oldChildShadowView.tag;
break;
}
case ShadowViewMutation::Remove: {
LOG(ERROR) << "REMOVE " << mutation.oldChildShadowView.tag << " " <<
mutation.index; break;
}
case ShadowViewMutation::Insert: {
LOG(ERROR) << "INSERT " << mutation.newChildShadowView.tag << " " <<
mutation.index; break;
}
case ShadowViewMutation::Update: {
LOG(ERROR) << "UPDATE " << mutation.newChildShadowView.tag;
break;
}
}
}*/
// Calculating mutations.
auto mutations1 = calculateShadowViewMutations(*rootNodeV1, *rootNodeV2);
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that inserting a node at the beginning
// produces a single "Insert" instruction, and no remove/insert (move)
// operations. All these nodes are laid out with absolute positioning, so
// moving them around does not change layout.
EXPECT_TRUE(mutations1.size() == 2);
EXPECT_TRUE(mutations1[0].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations1[0].newChildShadowView.tag == 100);
EXPECT_TRUE(mutations1[1].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations1[1].newChildShadowView.tag == 100);
EXPECT_TRUE(mutations1[1].index == 0);
// Calculating mutations.
auto mutations2 = calculateShadowViewMutations(*rootNodeV2, *rootNodeV3);
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that removing a node at the beginning
// produces a single remove (and delete) instruction, and no remove/insert
// (move) operations. All these nodes are laid out with absolute positioning,
// so moving them around does not change layout.
EXPECT_TRUE(mutations2.size() == 2);
EXPECT_TRUE(mutations2[0].type == ShadowViewMutation::Remove);
EXPECT_TRUE(mutations2[0].oldChildShadowView.tag == 100);
EXPECT_TRUE(mutations2[0].index == 0);
EXPECT_TRUE(mutations2[1].type == ShadowViewMutation::Delete);
EXPECT_TRUE(mutations2[1].oldChildShadowView.tag == 100);
// Calculating mutations.
auto mutations3 = calculateShadowViewMutations(*rootNodeV3, *rootNodeV4);
LOG(ERROR) << "Num mutations IN OLD TEST mutations3: " << mutations3.size();
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that removing a node in the middle
// produces a single remove (and delete) instruction, and no remove/insert
// (move) operations; and that simultaneously, we can insert a node at the
// end.
EXPECT_TRUE(mutations3.size() == 4);
EXPECT_TRUE(mutations3[0].type == ShadowViewMutation::Remove);
EXPECT_TRUE(mutations3[0].oldChildShadowView.tag == 102);
EXPECT_TRUE(mutations3[0].index == 1);
EXPECT_TRUE(mutations3[1].type == ShadowViewMutation::Delete);
EXPECT_TRUE(mutations3[1].oldChildShadowView.tag == 102);
EXPECT_TRUE(mutations3[2].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations3[2].newChildShadowView.tag == 104);
EXPECT_TRUE(mutations3[3].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations3[3].newChildShadowView.tag == 104);
EXPECT_TRUE(mutations3[3].index == 2);
// Calculating mutations.
auto mutations4 = calculateShadowViewMutations(*rootNodeV4, *rootNodeV5);
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that inserting a child at the middle, and
// at the end, and removing a node in the middle, produces the minimal set of
// instructions. All these nodes are laid out with absolute positioning, so
// moving them around does not change layout.
EXPECT_TRUE(mutations4.size() == 6);
EXPECT_TRUE(mutations4[0].type == ShadowViewMutation::Remove);
EXPECT_TRUE(mutations4[0].oldChildShadowView.tag == 103);
EXPECT_TRUE(mutations4[0].index == 1);
EXPECT_TRUE(mutations4[1].type == ShadowViewMutation::Delete);
EXPECT_TRUE(mutations4[1].oldChildShadowView.tag == 103);
EXPECT_TRUE(mutations4[2].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations4[2].newChildShadowView.tag == 100);
EXPECT_TRUE(mutations4[3].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations4[3].newChildShadowView.tag == 102);
EXPECT_TRUE(mutations4[4].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations4[4].newChildShadowView.tag == 100);
EXPECT_TRUE(mutations4[4].index == 1);
EXPECT_TRUE(mutations4[5].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations4[5].newChildShadowView.tag == 102);
EXPECT_TRUE(mutations4[5].index == 3);
auto mutations5 = calculateShadowViewMutations(*rootNodeV5, *rootNodeV6);
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that inserting TWO children in the middle
// produces the minimal set of instructions. All these nodes are laid out with
// absolute positioning, so moving them around does not change layout.
EXPECT_TRUE(mutations5.size() == 4);
EXPECT_TRUE(mutations5[0].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations5[0].newChildShadowView.tag == 103);
EXPECT_TRUE(mutations5[1].type == ShadowViewMutation::Create);
EXPECT_TRUE(mutations5[1].newChildShadowView.tag == 105);
EXPECT_TRUE(mutations5[2].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations5[2].newChildShadowView.tag == 103);
EXPECT_TRUE(mutations5[2].index == 2);
EXPECT_TRUE(mutations5[3].type == ShadowViewMutation::Insert);
EXPECT_TRUE(mutations5[3].newChildShadowView.tag == 105);
EXPECT_TRUE(mutations5[3].index == 3);
auto mutations6 = calculateShadowViewMutations(*rootNodeV6, *rootNodeV7);
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
// This test, in particular, ensures that a bug has been fixed: that with
// a particular sequence of inserts/removes/moves, we don't unintentionally
// create more "CREATE" mutations than necessary.
// The actual nodes that should be created in this transaction have a tag >
// 105.
EXPECT_TRUE(mutations6.size() == 25);
for (auto& i : mutations6) {
if (i.type == ShadowViewMutation::Create) {
EXPECT_TRUE(i.newChildShadowView.tag > 105);
}
}
}
/**
* Test reparenting mutation instruction generation.
* We cannot practically handle all possible use-cases here.
* It would be helpful to do verification with randomized trees, but it's
* much easier to do that in JS.
*/
TEST(MountingTest, testViewReparentingInstructionGeneration) {
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
contextContainer->insert(
"ReactNativeConfig", std::make_shared<EmptyReactNativeConfig>());
auto componentDescriptorParameters =
ComponentDescriptorParameters{eventDispatcher, contextContainer, nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
auto rootFamily =
rootComponentDescriptor.createFamily({Tag(1), SurfaceId(1), nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{RootShadowNode::defaultSharedProps()},
rootFamily)));
PropsParserContext parserContext{-1, *contextContainer};
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
Size{512, 0}, Size{512, std::numeric_limits<Float>::infinity()}},
LayoutContext{});
auto childA = makeNode(viewComponentDescriptor, 100, {});
auto childB = makeNode(viewComponentDescriptor, 101, {});
auto childC = makeNode(viewComponentDescriptor, 102, {});
auto childD = makeNode(viewComponentDescriptor, 103, {});
auto childE = makeNode(viewComponentDescriptor, 104, {});
auto childF = makeNode(viewComponentDescriptor, 105, {});
auto childG = makeNode(viewComponentDescriptor, 106, {});
auto childH = makeNode(viewComponentDescriptor, 107, {});
auto childI = makeNode(viewComponentDescriptor, 108, {});
auto childJ = makeNode(viewComponentDescriptor, 109, {});
auto childK = makeNode(viewComponentDescriptor, 110, {});
auto family =
viewComponentDescriptor.createFamily({10, SurfaceId(1), nullptr});
auto reparentedViewA = makeNode(
viewComponentDescriptor,
1000,
ShadowNode::ListOfShared{
childC->clone({}), childA->clone({}), childB->clone({})});
auto reparentedViewB = makeNode(
viewComponentDescriptor,
2000,
ShadowNode::ListOfShared{
childF->clone({}), childE->clone({}), childD->clone({})});
// Root -> G* -> H -> I -> J -> A* [nodes with * are _not_ flattened]
auto shadowNodeV1 = viewComponentDescriptor.createShadowNode(
ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})},
family);
// Root -> G* -> H* -> I -> J -> A* [nodes with * are _not_ flattened]
auto shadowNodeV2 = shadowNodeV1->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})});
// Root -> G* -> H -> I -> J -> A* [nodes with * are _not_ flattened]
auto shadowNodeV3 = shadowNodeV2->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})});
// The view is reparented 1 level down with a different sibling
// Root -> G* -> H* -> I* -> J -> [B*, A*] [nodes with * are _not_ flattened]
auto shadowNodeV4 = shadowNodeV3->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewB->clone({}),
reparentedViewA->clone(
{})})})})})})})})})})});
// The view is reparented 1 level further down with its order with the sibling
// swapped
// Root -> G* -> H* -> I* -> J* -> [A*, B*] [nodes with * are _not_ flattened]
auto shadowNodeV5 = shadowNodeV4->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone({}),
reparentedViewB->clone(
{})})})})})})})})})})});
// Injecting a tree into the root node.
auto rootNodeV1 = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV1})}));
auto rootNodeV2 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV1->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV2})}));
auto rootNodeV3 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV2->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV3})}));
auto rootNodeV4 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV3->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV4})}));
auto rootNodeV5 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV4->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV5})}));
// Layout
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV1{};
affectedLayoutableNodesV1.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV1)
->layoutIfNeeded(&affectedLayoutableNodesV1);
rootNodeV1->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV2{};
affectedLayoutableNodesV2.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV2)
->layoutIfNeeded(&affectedLayoutableNodesV2);
rootNodeV2->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV3{};
affectedLayoutableNodesV3.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV3)
->layoutIfNeeded(&affectedLayoutableNodesV3);
rootNodeV3->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV4{};
affectedLayoutableNodesV4.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV4)
->layoutIfNeeded(&affectedLayoutableNodesV4);
rootNodeV4->sealRecursive();
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV5{};
affectedLayoutableNodesV5.reserve(1024);
std::const_pointer_cast<RootShadowNode>(rootNodeV5)
->layoutIfNeeded(&affectedLayoutableNodesV5);
rootNodeV5->sealRecursive();
// Calculating mutations.
auto mutations1 = calculateShadowViewMutations(*rootNodeV1, *rootNodeV2);
EXPECT_EQ(mutations1.size(), 5);
EXPECT_EQ(mutations1[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations1[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations1[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations1[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations1[2].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations1[2].newChildShadowView.tag, 107);
EXPECT_EQ(mutations1[3].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations1[3].newChildShadowView.tag, 107);
EXPECT_EQ(mutations1[4].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations1[4].newChildShadowView.tag, 1000);
auto mutations2 = calculateShadowViewMutations(*rootNodeV2, *rootNodeV3);
EXPECT_EQ(mutations2.size(), 5);
EXPECT_EQ(mutations2[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations2[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations2[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations2[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations2[2].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations2[2].oldChildShadowView.tag, 107);
EXPECT_EQ(
mutations2[3].type,
ShadowViewMutation::Delete); // correct, 107 is removed from tree entirely
EXPECT_EQ(mutations2[3].oldChildShadowView.tag, 107);
EXPECT_EQ(mutations2[4].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations2[4].newChildShadowView.tag, 1000);
auto mutations3 = calculateShadowViewMutations(*rootNodeV3, *rootNodeV4);
// between these two trees, lots of new nodes are created and inserted - this
// is all correct, and this is the minimal amount of mutations
EXPECT_EQ(mutations3.size(), 15);
EXPECT_EQ(mutations3[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations3[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations3[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations3[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations3[2].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[2].newChildShadowView.tag, 107);
EXPECT_EQ(mutations3[3].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[3].newChildShadowView.tag, 2000);
EXPECT_EQ(mutations3[4].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[4].newChildShadowView.tag, 108);
EXPECT_EQ(mutations3[5].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[5].newChildShadowView.tag, 105);
EXPECT_EQ(mutations3[6].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[6].newChildShadowView.tag, 104);
EXPECT_EQ(mutations3[7].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[7].newChildShadowView.tag, 103);
EXPECT_EQ(mutations3[8].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[8].newChildShadowView.tag, 105);
EXPECT_EQ(mutations3[9].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[9].newChildShadowView.tag, 104);
EXPECT_EQ(mutations3[10].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[10].newChildShadowView.tag, 103);
EXPECT_EQ(mutations3[11].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[11].newChildShadowView.tag, 107);
EXPECT_EQ(mutations3[12].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[12].newChildShadowView.tag, 108);
EXPECT_EQ(mutations3[13].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[13].newChildShadowView.tag, 2000);
EXPECT_EQ(mutations3[14].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[14].newChildShadowView.tag, 1000);
auto mutations4 = calculateShadowViewMutations(*rootNodeV4, *rootNodeV5);
EXPECT_EQ(mutations4.size(), 9);
EXPECT_EQ(mutations4[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations4[1].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[1].oldChildShadowView.tag, 107);
EXPECT_EQ(mutations4[2].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[2].oldChildShadowView.tag, 108);
EXPECT_EQ(mutations4[3].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations4[3].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations4[4].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations4[4].oldChildShadowView.tag, 2000);
EXPECT_EQ(mutations4[5].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations4[5].newChildShadowView.tag, 109);
EXPECT_EQ(mutations4[6].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[6].newChildShadowView.tag, 109);
EXPECT_EQ(mutations4[7].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[7].newChildShadowView.tag, 1000);
EXPECT_EQ(mutations4[8].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[8].newChildShadowView.tag, 2000);
}
} // namespace facebook::react

View File

@@ -0,0 +1,244 @@
/*
* 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 <algorithm>
#include <memory>
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs.h>
namespace facebook::react {
class OrderIndexTest : public ::testing::Test {
protected:
ComponentBuilder builder_;
std::shared_ptr<RootShadowNode> rootShadowNode_;
std::shared_ptr<ViewShadowNode> nodeA_;
std::shared_ptr<ViewShadowNode> nodeB_;
std::shared_ptr<ViewShadowNode> nodeC_;
std::shared_ptr<ViewShadowNode> nodeD_;
std::shared_ptr<RootShadowNode> currentRootShadowNode_;
StubViewTree currentStubViewTree_;
OrderIndexTest() : builder_(simpleComponentBuilder()) {
auto element = Element<RootShadowNode>()
.reference(rootShadowNode_)
.tag(1)
.children({
Element<ViewShadowNode>().tag(2).reference(nodeA_),
Element<ViewShadowNode>().tag(3).reference(nodeB_),
Element<ViewShadowNode>().tag(4).reference(nodeC_),
Element<ViewShadowNode>().tag(5).reference(nodeD_),
});
builder_.build(element);
mutateViewShadowNodeProps_(nodeA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeC_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
currentRootShadowNode_ = rootShadowNode_;
currentRootShadowNode_->layoutIfNeeded();
currentStubViewTree_ =
buildStubViewTreeWithoutUsingDifferentiator(*currentRootShadowNode_);
}
void mutateViewShadowNodeProps_(
const std::shared_ptr<ViewShadowNode>& node,
std::function<void(ViewProps& props)> callback) {
rootShadowNode_ =
std::static_pointer_cast<RootShadowNode>(rootShadowNode_->cloneTree(
node->getFamily(), [&](const ShadowNode& oldShadowNode) {
auto viewProps = std::make_shared<ViewShadowNodeProps>();
callback(*viewProps);
return oldShadowNode.clone(ShadowNodeFragment{viewProps});
}));
}
void testViewTree_(
const std::function<void(StubViewTree const& viewTree)>& callback) {
rootShadowNode_->layoutIfNeeded();
callback(buildStubViewTreeUsingDifferentiator(*rootShadowNode_));
auto mutations =
calculateShadowViewMutations(*currentRootShadowNode_, *rootShadowNode_);
currentRootShadowNode_ = rootShadowNode_;
currentStubViewTree_.mutate(mutations);
callback(currentStubViewTree_);
}
};
TEST_F(OrderIndexTest, defaultOrderIsDocumentOrder) {
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeD_->getTag());
});
}
TEST_F(OrderIndexTest, basicZIndex) {
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = 10; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = 1; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 2; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeB_->getTag());
});
}
TEST_F(OrderIndexTest, negativeZIndex) {
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = -10; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = -1; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 2; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeA_->getTag());
});
}
TEST_F(OrderIndexTest, zeroZIndex) {
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = 0; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 0; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeD_->getTag());
});
}
TEST_F(OrderIndexTest, staticBehindNonStatic) {
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeC_->getTag());
});
}
TEST_F(OrderIndexTest, zIndexStaticBehindNonStatic) {
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = -1; });
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeB_->getTag());
});
}
TEST_F(OrderIndexTest, staticDoesNotGetZIndex) {
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
props.zIndex = 5;
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
props.zIndex = -5;
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeC_->getTag());
});
}
} // namespace facebook::react

View File

@@ -0,0 +1,420 @@
/*
* 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 <vector>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <react/config/ReactNativeConfig.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs.h>
#include <react/test_utils/Entropy.h>
#include <react/test_utils/shadowTreeGeneration.h>
// Uncomment when random test blocks are uncommented below.
// #include <algorithm>
// #include <random>
namespace facebook::react {
static void testShadowNodeTreeLifeCycle(
uint_fast32_t seed,
int treeSize,
int repeats,
int stages) {
auto entropy = seed == 0 ? Entropy() : Entropy(seed);
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
auto componentDescriptorParameters =
ComponentDescriptorParameters{eventDispatcher, contextContainer, nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
PropsParserContext parserContext{-1, *contextContainer};
auto allNodes = std::vector<ShadowNode::Shared>{};
for (int i = 0; i < repeats; i++) {
allNodes.clear();
auto family =
rootComponentDescriptor.createFamily({Tag(1), SurfaceId(1), nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{RootShadowNode::defaultSharedProps()},
family)));
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
Size{512, 0}, Size{512, std::numeric_limits<Float>::infinity()}},
LayoutContext{});
// Generation of a random tree.
auto singleRootChildNode =
generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize);
// Injecting a tree into the root node.
auto currentRootNode = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{singleRootChildNode})}));
// Building an initial view hierarchy.
auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*emptyRootNode);
viewTree.mutate(
calculateShadowViewMutations(*emptyRootNode, *currentRootNode));
for (int j = 0; j < stages; j++) {
auto nextRootNode = currentRootNode;
// Mutating the tree.
alterShadowTree(
entropy,
nextRootNode,
{
&messWithChildren,
&messWithYogaStyles,
&messWithLayoutableOnlyFlag,
});
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
// Laying out the tree.
std::const_pointer_cast<RootShadowNode>(nextRootNode)
->layoutIfNeeded(&affectedLayoutableNodes);
nextRootNode->sealRecursive();
allNodes.push_back(nextRootNode);
// Calculating mutations.
auto mutations =
calculateShadowViewMutations(*currentRootNode, *nextRootNode);
// Make sure that in a single frame, a DELETE for a
// view is not followed by a CREATE for the same view.
{
std::vector<int> deletedTags{};
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Delete) {
deletedTags.push_back(mutation.oldChildShadowView.tag);
}
}
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Create) {
if (std::find(
deletedTags.begin(),
deletedTags.end(),
mutation.newChildShadowView.tag) != deletedTags.end()) {
LOG(ERROR) << "Deleted tag was recreated in mutations list: ["
<< mutation.newChildShadowView.tag << "]";
react_native_assert(false);
}
}
}
}
// Mutating the view tree.
viewTree.mutate(mutations);
// Building a view tree to compare with.
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR) << "Entropy seed: " << entropy.getSeed() << "\n";
// There are some issues getting `getDebugDescription` to compile
// under test on Android for now.
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "Shadow Tree before: \n"
<< currentRootNode->getDebugDescription();
LOG(ERROR) << "Shadow Tree after: \n"
<< nextRootNode->getDebugDescription();
LOG(ERROR) << "View Tree before: \n"
<< getDebugDescription(viewTree.getRootStubView(), {});
LOG(ERROR) << "View Tree after: \n"
<< getDebugDescription(
rebuiltViewTree.getRootStubView(), {});
LOG(ERROR) << "Mutations:"
<< "\n"
<< getDebugDescription(mutations, {});
#endif
react_native_assert(false);
}
currentRootNode = nextRootNode;
}
}
SUCCEED();
}
static void testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
uint_fast32_t seed,
int treeSize,
int repeats,
int stages) {
auto entropy = seed == 0 ? Entropy() : Entropy(seed);
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
contextContainer->insert(
"ReactNativeConfig", std::make_shared<EmptyReactNativeConfig>());
auto componentDescriptorParameters =
ComponentDescriptorParameters{eventDispatcher, contextContainer, nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
PropsParserContext parserContext{-1, *contextContainer};
auto allNodes = std::vector<ShadowNode::Shared>{};
for (int i = 0; i < repeats; i++) {
allNodes.clear();
auto family =
rootComponentDescriptor.createFamily({Tag(1), SurfaceId(1), nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{RootShadowNode::defaultSharedProps()},
family)));
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
Size{512, 0}, Size{512, std::numeric_limits<Float>::infinity()}},
LayoutContext{});
// Generation of a random tree.
auto singleRootChildNode =
generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize);
// Injecting a tree into the root node.
auto currentRootNode = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{singleRootChildNode})}));
// Building an initial view hierarchy.
auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*emptyRootNode);
viewTree.mutate(
calculateShadowViewMutations(*emptyRootNode, *currentRootNode));
for (int j = 0; j < stages; j++) {
auto nextRootNode = currentRootNode;
// Mutating the tree.
alterShadowTree(
entropy,
nextRootNode,
{
&messWithYogaStyles,
&messWithLayoutableOnlyFlag,
});
alterShadowTree(entropy, nextRootNode, &messWithNodeFlattenednessFlags);
alterShadowTree(entropy, nextRootNode, &messWithChildren);
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
// Laying out the tree.
std::const_pointer_cast<RootShadowNode>(nextRootNode)
->layoutIfNeeded(&affectedLayoutableNodes);
nextRootNode->sealRecursive();
allNodes.push_back(nextRootNode);
// Calculating mutations.
auto mutations =
calculateShadowViewMutations(*currentRootNode, *nextRootNode);
// Make sure that in a single frame, a DELETE for a
// view is not followed by a CREATE for the same view.
{
std::vector<int> deletedTags{};
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Delete) {
deletedTags.push_back(mutation.oldChildShadowView.tag);
}
}
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Create) {
if (std::find(
deletedTags.begin(),
deletedTags.end(),
mutation.newChildShadowView.tag) != deletedTags.end()) {
LOG(ERROR) << "Deleted tag was recreated in mutations list: ["
<< mutation.newChildShadowView.tag << "]";
react_native_assert(false);
}
}
}
}
// Mutating the view tree.
viewTree.mutate(mutations);
// Building a view tree to compare with.
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR) << "Entropy seed: " << entropy.getSeed() << "\n";
// There are some issues getting `getDebugDescription` to compile
// under test on Android for now.
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "Shadow Tree before: \n"
<< currentRootNode->getDebugDescription();
LOG(ERROR) << "Shadow Tree after: \n"
<< nextRootNode->getDebugDescription();
LOG(ERROR) << "View Tree before: \n"
<< getDebugDescription(viewTree.getRootStubView(), {});
LOG(ERROR) << "View Tree after: \n"
<< getDebugDescription(
rebuiltViewTree.getRootStubView(), {});
LOG(ERROR) << "Mutations:"
<< "\n"
<< getDebugDescription(mutations, {});
#endif
react_native_assert(false);
}
currentRootNode = nextRootNode;
}
}
SUCCEED();
}
} // namespace facebook::react
using namespace facebook::react;
TEST(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
/* seed */ 0,
/* size */ 512,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener2) {
testShadowNodeTreeLifeCycle(
/* seed */ 1,
/* size */ 512,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
stableSmallerTreeMoreIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
/* seed */ 0,
/* size */ 16,
/* repeats */ 512,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableSmallerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 32,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableBiggerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 256,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 32,
/* repeats */ 512,
/* stages */ 32);
}
// failing test case found 4-25-2021
TEST(
ShadowTreeLifecycleTest,
unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1167342011,
/* size */ 32,
/* repeats */ 512,
/* stages */ 32);
}
// You may uncomment this - locally only! - to generate failing seeds.
// TEST(
// ShadowTreeLifecycleTest,
// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflatteningManyRandom)
// {
// std::random_device device;
// for (int i = 0; i < 10; i++) {
// uint_fast32_t seed = device();
// LOG(ERROR) << "Seed: " << seed;
// testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
// /* seed */ seed,
// /* size */ 32,
// /* repeats */ 512,
// /* stages */ 32);
// }
// }

View File

@@ -0,0 +1,803 @@
/*
* 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 <algorithm>
#include <memory>
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs.h>
namespace facebook::react {
class StackingContextTest : public ::testing::Test {
protected:
ComponentBuilder builder_;
std::shared_ptr<RootShadowNode> rootShadowNode_;
std::shared_ptr<ViewShadowNode> nodeA_;
std::shared_ptr<ViewShadowNode> nodeAA_;
std::shared_ptr<ViewShadowNode> nodeB_;
std::shared_ptr<ViewShadowNode> nodeBA_;
std::shared_ptr<ViewShadowNode> nodeBB_;
std::shared_ptr<ViewShadowNode> nodeBBA_;
std::shared_ptr<ViewShadowNode> nodeBBB_;
std::shared_ptr<ViewShadowNode> nodeBC_;
std::shared_ptr<ViewShadowNode> nodeBD_;
std::shared_ptr<RootShadowNode> currentRootShadowNode_;
StubViewTree currentStubViewTree_;
StackingContextTest() : builder_(simpleComponentBuilder()) {
// ┌────────────── (Root) ──────────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// └────────────────────────────────────┘
// clang-format off
auto element =
Element<RootShadowNode>()
.reference(rootShadowNode_)
.tag(1)
.children({
Element<ViewShadowNode>()
.tag(2)
.reference(nodeA_)
.children({
Element<ViewShadowNode>()
.tag(3)
.reference(nodeAA_)
}),
Element<ViewShadowNode>()
.tag(4)
.reference(nodeB_)
.children({
Element<ViewShadowNode>()
.tag(5)
.reference(nodeBA_),
Element<ViewShadowNode>()
.tag(6)
.reference(nodeBB_)
.children({
Element<ViewShadowNode>()
.tag(7)
.reference(nodeBBA_),
Element<ViewShadowNode>()
.tag(8)
.reference(nodeBBB_)
}),
Element<ViewShadowNode>()
.tag(9)
.reference(nodeBC_),
Element<ViewShadowNode>()
.tag(10)
.reference(nodeBD_)
})
});
// clang-format on
builder_.build(element);
currentRootShadowNode_ = rootShadowNode_;
currentRootShadowNode_->layoutIfNeeded();
currentStubViewTree_ =
buildStubViewTreeWithoutUsingDifferentiator(*currentRootShadowNode_);
}
void mutateViewShadowNodeProps_(
const std::shared_ptr<ViewShadowNode>& node,
std::function<void(ViewProps& props)> callback) {
rootShadowNode_ =
std::static_pointer_cast<RootShadowNode>(rootShadowNode_->cloneTree(
node->getFamily(), [&](const ShadowNode& oldShadowNode) {
auto viewProps = std::make_shared<ViewShadowNodeProps>();
callback(*viewProps);
return oldShadowNode.clone(ShadowNodeFragment{viewProps});
}));
}
void testViewTree_(
const std::function<void(StubViewTree const& viewTree)>& callback) {
rootShadowNode_->layoutIfNeeded();
callback(buildStubViewTreeUsingDifferentiator(*rootShadowNode_));
callback(buildStubViewTreeWithoutUsingDifferentiator(*rootShadowNode_));
auto mutations =
calculateShadowViewMutations(*currentRootShadowNode_, *rootShadowNode_);
currentRootShadowNode_ = rootShadowNode_;
currentStubViewTree_.mutate(mutations);
callback(currentStubViewTree_);
}
};
TEST_F(StackingContextTest, defaultPropsMakeEverythingFlattened) {
testViewTree_([](const StubViewTree& viewTree) {
// 1 view in total.
EXPECT_EQ(viewTree.size(), 1);
// The root view has no subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 0);
});
}
TEST_F(StackingContextTest, mostPropsDoNotForceViewsToMaterialize) {
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ───────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ padding: 10; ┃ ┃ │ │ │
// │ ┃ ┃ margin: 9001; ┃ ┃ │ │ │
// │ ┃ ┃ position: absolute; ┃ ┃ │ │ │
// │ ┃ ┃ shadowRadius: 10; ┃ ┃ │ │ │
// │ ┃ ┃ shadowOffset: [42, 42]; ┃ ┃ │ │ │
// │ ┃ ┃ backgroundColor: clear; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ zIndex: 42; ┃ ┃ │ │ │
// │ ┃ ┃ margin: 42; ┃ ┃ │ │ │
// │ ┃ ┃ shadowColor: clear; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ No observable side-effects. │
// │ ┃ ┃ ┃ ┃ │━━━▶│ No views are generated. │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ borderRadii: 42; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ borderColor: black; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ onLayout: true; ┃ ┃ │ │ │
// │ ┃ ┃ hitSlop: 42; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPadding(yoga::Edge::All, yoga::value::points(42));
yogaStyle.setMargin(yoga::Edge::All, yoga::value::points(42));
yogaStyle.setPositionType(yoga::PositionType::Absolute);
props.shadowRadius = 42;
props.shadowOffset = Size{42, 42};
props.backgroundColor = clearColor();
});
mutateViewShadowNodeProps_(nodeBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
props.zIndex = 42;
yogaStyle.setPositionType(yoga::PositionType::Static);
yogaStyle.setMargin(yoga::Edge::All, yoga::value::points(42));
props.shadowColor = clearColor();
props.shadowOpacity = 0.42;
});
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.borderRadii.all = 42;
props.borderColors.all = blackColor();
});
mutateViewShadowNodeProps_(nodeBD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
props.onLayout = true;
props.hitSlop = EdgeInsets{42, 42, 42, 42};
yogaStyle.setPositionType(yoga::PositionType::Static);
});
testViewTree_([](const StubViewTree& viewTree) {
// 1 view in total.
EXPECT_EQ(viewTree.size(), 1);
// The root view has no subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 0);
});
}
TEST_F(StackingContextTest, somePropsForceViewsToMaterialize1) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ ┃ backgroundColor: black; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ backgroundColor: white; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ shadowColor: black; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeAA_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(
nodeBA_, [](ViewProps& props) { props.backgroundColor = whiteColor(); });
mutateViewShadowNodeProps_(
nodeBBA_, [](ViewProps& props) { props.shadowColor = blackColor(); });
testViewTree_([](const StubViewTree& viewTree) {
// 4 views in total.
EXPECT_EQ(viewTree.size(), 4);
// The root view has all 3 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 3);
// The root view subviews are [3, 5, 7].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 3);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 7);
});
}
TEST_F(StackingContextTest, somePropsForceViewsToMaterialize2) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ A (tag: 2) ━━━━━━━━━━━━┓ │
// │ ┃ backgroundColor: black; ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┃ pointerEvents: none; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ B (tag: 4) ━━━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ testId: "42" ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BB (tag: 6) ━━━━━━━━━━━┓ │
// │ ┃ ┃ nativeId: "42" ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ backgroundColor: black; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBB (tag: 8) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ transform: scale(2); ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━━┓ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ zIndex: 42; ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┏━ BD (tag: 10) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ shadowColor: black; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ opacity: 0.42; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
props.pointerEvents = PointerEventsMode::None;
});
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.testId = "42"; });
mutateViewShadowNodeProps_(
nodeBA_, [](ViewProps& props) { props.nativeId = "42"; });
mutateViewShadowNodeProps_(
nodeBB_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
props.transform = Transform::Scale(2, 2, 2);
});
mutateViewShadowNodeProps_(nodeBBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 42;
});
mutateViewShadowNodeProps_(
nodeBC_, [](ViewProps& props) { props.shadowColor = blackColor(); });
mutateViewShadowNodeProps_(
nodeBD_, [](ViewProps& props) { props.opacity = 0.42; });
testViewTree_([](const StubViewTree& viewTree) {
// 10 views in total.
EXPECT_EQ(viewTree.size(), 10);
// The root view has all 9 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 9);
});
}
TEST_F(StackingContextTest, zIndexAndFlattenedNodes) {
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ───────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BD (tag: 10) ━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━━┓ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ zIndex: 9001; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBB (tag: 8) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ zIndex: 9000; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ zIndex: 8999; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ zIndex: 8998; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ │
// │ ┃ ┃ zIndex: 8997; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ │
// │ ┃ ┃ zIndex: 8996; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 9001;
});
mutateViewShadowNodeProps_(nodeBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 9000;
});
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8999;
});
mutateViewShadowNodeProps_(nodeBBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8998;
});
mutateViewShadowNodeProps_(nodeBC_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8997;
});
mutateViewShadowNodeProps_(nodeBD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8996;
});
testViewTree_([](const StubViewTree& viewTree) {
// 7 views in total.
EXPECT_EQ(viewTree.size(), 7);
// The root view has all 6 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 6);
// The root view subviews are [10, 9, 8, 7, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 8);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 7);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(5)->tag, 3);
});
// And now let's make BB to form a Stacking Context with small order-index.
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ──────────┐
// │ ┌─ A (tag: 2) ───────────────────┐ │ │ ┏━ BB (tag: 6) ━━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┃ ┏━ BBB (tag: 8) ━━━━━┓ ┃ │
// │ │ ┌─ AA (tag: 3) ──────────────┐ │ │ │ ┃ ┃ #View ┃ ┃ │
// │ │ │ position: relative; │ │ │ │ ┃ ┃ #StackingContext ┃ ┃ │
// │ │ │ zIndex: 9001; │ │ │ │ ┃ ┃ ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┗━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ │ │ │ │ │ │ ┃ ┏━ BBA (tag: 7) ━━━━━┓ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ #View ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ #StackingContext ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ ┃ ┃ │
// │ │ └────────────────────────────┘ │ │ │ ┃ ┗━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ └────────────────────────────────┘ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┌─ B (tag: 4) ───────────────────┐ │ │ ┏━ BD (tag: 10) ━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ BA (tag: 5) ──────────────┐ │ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━┓ │
// │ │ │ position: relative; │ │ │ │ ┃ #View ┃ │
// │ │ │ zIndex: 9000; │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ └────────────────────────────┘ │ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━┓ │
// │ │ ╔═ BB (tag: 6) ══════════════╗ │ │ │ ┃ #View ┃ │
// │ │ ║ *** position: relative; ║ │ │ │ ┃ #StackingContext ┃ │
// │ │ ║ *** zIndex: 42; ║ │ │━━━━▶│ ┃ ┃ │
// │ │ ║ ║ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ║ ║ │ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━┓ │
// │ │ ║ ┌─ BBA (tag: 7) ─────────┐ ║ │ │ │ ┃ #View ┃ │
// │ │ ║ │ position: relative; │ ║ │ │ │ ┃ #StackingContext ┃ │
// │ │ ║ │ zIndex: 8999; │ ║ │ │ │ ┃ ┃ │
// │ │ ║ │ │ ║ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ║ ┌─ BBB (tag: 8) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8998; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ╚════════════════════════════╝ │ │ │ │
// │ │ ┌─ BC (tag: 9) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8997; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ┌─ BD (tag: 10) ─────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8996; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ └────────────────────────────────┘ │ │ │
// └────────────────────────────────────┘ └────────────────────────────┘
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 42;
});
testViewTree_([](const StubViewTree& viewTree) {
// 8 views in total.
EXPECT_EQ(viewTree.size(), 8);
// The root view has 5 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 5);
// The root view subviews are [6, 10, 9, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 6);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 3);
auto& view6 = viewTree.getStubView(6);
EXPECT_EQ(view6.children.size(), 2);
EXPECT_EQ(view6.children.at(0)->tag, 8);
EXPECT_EQ(view6.children.at(1)->tag, 7);
});
// And now, let's revert it back.
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.zIndex = {};
});
testViewTree_([](const StubViewTree& viewTree) {
// 7 views in total.
EXPECT_EQ(viewTree.size(), 7);
// The root view has all 6 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 6);
// The root view subviews are [10, 9, 8, 7, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 8);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 7);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(5)->tag, 3);
});
// And now, let's hide BB completety.
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ──────────┐
// │ ┌─ A (tag: 2) ───────────────────┐ │ │ ┏━ BD (tag: 10) ━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ AA (tag: 3) ──────────────┐ │ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━┓ │
// │ │ │ position: relative; │ │ │ │ ┃ #View ┃ │
// │ │ │ zIndex: 9001; │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ │ │ │ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━┓ │
// │ │ │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ └────────────────────────────┘ │ │ │ ┃ ┃ │
// │ └────────────────────────────────┘ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┌─ B (tag: 4) ───────────────────┐ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ BA (tag: 5) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 9000; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ╔═ BB (tag: 6) ══════════════╗ │ │ │ │
// │ │ ║ *** display: none; ║ │ │ │ │
// │ │ ║ ║ │ │━━━━▶│ │
// │ │ ║ ║ │ │ │ │
// │ │ ║ ║ │ │ │ │
// │ │ ║ ┌─ BBA (tag: 7) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8999; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ║ ┌─ BBB (tag: 8) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8998; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ╚════════════════════════════╝ │ │ │ │
// │ │ ┌─ BC (tag: 9) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8997; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ┌─ BD (tag: 10) ─────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8996; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ └────────────────────────────────┘ │ │ │
// └────────────────────────────────────┘ └────────────────────────────┘
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setDisplay(yoga::Display::None);
});
testViewTree_([](const StubViewTree& viewTree) {
#ifdef ANDROID
// T153547836: Android still mounts views with
// ShadowNodeTraits::Trait::Hidden
EXPECT_EQ(viewTree.size(), 8);
// nodeBB_ forms a stacking context
EXPECT_EQ(viewTree.getRootStubView().children.size(), 5);
// The root view subviews are [6, 10, 9, 5].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 6);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 3);
#else
EXPECT_EQ(viewTree.size(), 5);
// The root view has all 4 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
// The root view subviews are [6, 10, 9, 5].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 3);
#endif
});
}
} // namespace facebook::react

View File

@@ -0,0 +1,301 @@
/*
* 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 <memory>
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/mounting/MountingCoordinator.h>
#include <react/renderer/mounting/ShadowTree.h>
#include <react/renderer/mounting/ShadowTreeDelegate.h>
#include <react/renderer/element/testUtils.h>
using namespace facebook::react;
class DummyShadowTreeDelegate : public ShadowTreeDelegate {
public:
RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree& /*shadowTree*/,
const RootShadowNode::Shared& /*oldRootShadowNode*/,
const RootShadowNode::Unshared& newRootShadowNode) const override {
return newRootShadowNode;
};
void shadowTreeDidFinishTransaction(
MountingCoordinator::Shared 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<bool> {
public:
StateReconciliationTest() : builder_(simpleComponentBuilder()) {
CoreFeatures::enableClonelessStateProgression = GetParam();
}
ComponentBuilder builder_;
};
TEST_P(StateReconciliationTest, testStateReconciliation) {
auto shadowNodeA = std::shared_ptr<RootShadowNode>{};
auto shadowNodeAA = std::shared_ptr<ViewShadowNode>{};
auto shadowNodeAB = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.reference(shadowNodeA)
.children({
Element<ViewShadowNode>()
.reference(shadowNodeAA),
Element<ScrollViewShadowNode>()
.reference(shadowNodeAB)
});
// clang-format on
ContextContainer contextContainer{};
auto shadowNode = builder_.build(element);
auto rootShadowNodeState1 = shadowNode->ShadowNode::clone({});
auto& scrollViewComponentDescriptor = shadowNodeAB->getComponentDescriptor();
auto& family = shadowNodeAB->getFamily();
auto state1 = shadowNodeAB->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState1);
},
{true});
EXPECT_EQ(state1->getMostRecentState(), state1);
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState1, family)->getState(), state1);
auto state2 = scrollViewComponentDescriptor.createState(
family, std::make_shared<const ScrollViewState>());
auto rootShadowNodeState2 =
shadowNode->cloneTree(family, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone(
{ShadowNodeFragment::propsPlaceholder(),
ShadowNodeFragment::childrenPlaceholder(),
state2});
});
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState2, family)->getState(), state2);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState2);
},
{true});
EXPECT_EQ(state1->getMostRecentState(), state2);
EXPECT_EQ(state2->getMostRecentState(), state2);
auto state3 = scrollViewComponentDescriptor.createState(
family, std::make_shared<const ScrollViewState>());
auto rootShadowNodeState3 = rootShadowNodeState2->cloneTree(
family, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone(
{ShadowNodeFragment::propsPlaceholder(),
ShadowNodeFragment::childrenPlaceholder(),
state3});
});
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState3, family)->getState(), state3);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState3);
},
{true});
EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state3);
EXPECT_EQ(state1->getMostRecentState(), state3);
EXPECT_EQ(state2->getMostRecentState(), state3);
EXPECT_EQ(state3->getMostRecentState(), state3);
// This is the core part of the whole test.
// Here we commit the old tree but we expect that the state associated with
// the node will stay the same (newer that the old tree has).
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState2);
},
{true});
// Warning:
// there is important semantic difference with the approach. With the old
// algorithm, you couldn't go back to a shadow node with old state. New state
// was always enforced when state reconciliation was enabled. The clone-less
// algorithm does not support that, because it can't mutate such a node in
// place.
if (!GetParam()) {
EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state3);
} else {
EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state2);
}
}
TEST_P(StateReconciliationTest, testCloneslessStateReconciliationDoesntClone) {
auto shadowNodeA = std::shared_ptr<RootShadowNode>{};
auto shadowNodeAA = std::shared_ptr<ViewShadowNode>{};
auto shadowNodeAB = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.reference(shadowNodeA)
.children({
Element<ViewShadowNode>()
.reference(shadowNodeAA),
Element<ScrollViewShadowNode>()
.reference(shadowNodeAB)
});
// clang-format on
ContextContainer contextContainer{};
auto rootShadowNode1 = builder_.build(element);
auto& scrollViewComponentDescriptor = shadowNodeAB->getComponentDescriptor();
auto& family = shadowNodeAB->getFamily();
auto state1 = shadowNodeAB->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode1);
},
{true});
EXPECT_EQ(state1->getMostRecentState(), state1);
EXPECT_EQ(findDescendantNode(*rootShadowNode1, family)->getState(), state1);
auto state2 = scrollViewComponentDescriptor.createState(
family, std::make_shared<const ScrollViewState>());
auto rootShadowNode2 =
rootShadowNode1->cloneTree(family, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone(
{ShadowNodeFragment::propsPlaceholder(),
ShadowNodeFragment::childrenPlaceholder(),
state2});
});
EXPECT_EQ(findDescendantNode(*rootShadowNode2, family)->getState(), state2);
EXPECT_EQ(state1->getMostRecentState(), state1);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode2);
},
{true});
EXPECT_EQ(state1->getMostRecentState(), state2);
EXPECT_EQ(state2->getMostRecentState(), state2);
ShadowNode::Unshared newlyClonedShadowNode;
auto rootShadowNodeClonedFromReact =
rootShadowNode2->cloneTree(family, [&](const ShadowNode& oldShadowNode) {
newlyClonedShadowNode = oldShadowNode.clone({});
return newlyClonedShadowNode;
});
auto state3 = scrollViewComponentDescriptor.createState(
family, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate =
rootShadowNode2->cloneTree(family, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone(
{ShadowNodeFragment::propsPlaceholder(),
ShadowNodeFragment::childrenPlaceholder(),
state3});
});
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{});
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{true});
auto scrollViewShadowNode = findDescendantNode(shadowTree, family);
EXPECT_EQ(scrollViewShadowNode->getState(), state3);
if (GetParam()) {
// Checking that newlyClonedShadowNode was not cloned unnecessarly by state
// progression. This fails with the old algorithm.
EXPECT_EQ(scrollViewShadowNode, newlyClonedShadowNode.get());
}
}
INSTANTIATE_TEST_SUITE_P(
StateReconciliationTestInstantiation,
StateReconciliationTest,
testing::Values(false, true));