diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-hiddenSubtrees-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-hiddenSubtrees-itest.js
new file mode 100644
index 000000000000..858a32e038ad
--- /dev/null
+++ b/packages/react-native/Libraries/Components/View/__tests__/View-hiddenSubtrees-itest.js
@@ -0,0 +1,208 @@
+/**
+ * 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.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
+
+import * as Fantom from '@react-native/fantom';
+import * as React from 'react';
+import {Text, View} from 'react-native';
+
+type ScreenState = {
+ rows: number,
+ label: string,
+ nestedHidden: boolean,
+ absolute: boolean,
+};
+
+function Screen({
+ hidden,
+ state,
+}: {
+ hidden: boolean,
+ state: ScreenState,
+}): React.Node {
+ const rows = [];
+ for (let i = 0; i < state.rows; i++) {
+ rows.push(
+
+
+
+
+
+ {state.label} {i}
+
+
+
+ ,
+ );
+ }
+ return (
+
+
+
+
+ {rows}
+
+ );
+}
+
+function App({
+ visible,
+ headerHeight,
+ screens,
+}: {
+ visible: number,
+ headerHeight: number,
+ screens: ReadonlyArray,
+}): React.Node {
+ return (
+
+
+
+ {screens.map((state, i) => (
+
+ ))}
+
+
+ );
+}
+
+function renderedOutput(root: Fantom.Root) {
+ return root
+ .getRenderedOutput({includeLayoutMetrics: true, includeRoot: false})
+ .toJSON();
+}
+
+const INITIAL: ReadonlyArray = [
+ {rows: 3, label: 'A', nestedHidden: false, absolute: true},
+ {rows: 3, label: 'B', nestedHidden: false, absolute: true},
+ {rows: 3, label: 'C', nestedHidden: true, absolute: false},
+];
+
+describe('display: none subtrees', () => {
+ it('lays out a screen shown again after updates while it was hidden', () => {
+ const root = Fantom.createRoot({viewportWidth: 400, viewportHeight: 800});
+ let screens = INITIAL;
+ let headerHeight = 50;
+ const render = (visible: number) => {
+ Fantom.runTask(() => {
+ root.render(
+ ,
+ );
+ });
+ };
+
+ // Lay out every screen once, then hide all but the first.
+ render(1);
+ render(2);
+ render(0);
+
+ // Updates to the visible screen, the layout around the hidden screens,
+ // and the hidden screens themselves.
+ for (let i = 0; i < 5; i++) {
+ headerHeight = 50 + (i % 2) * 10;
+ render(0);
+ }
+ screens = [
+ INITIAL[0],
+ {rows: 5, label: 'B2', nestedHidden: true, absolute: true},
+ {rows: 1, label: 'C2', nestedHidden: false, absolute: false},
+ ];
+ render(0);
+ headerHeight = 70;
+ render(0);
+
+ render(1);
+ const shownB = renderedOutput(root);
+ render(2);
+ const shownC = renderedOutput(root);
+
+ // The same final states, rendered without ever hiding the screens.
+ const reference = Fantom.createRoot({
+ viewportWidth: 400,
+ viewportHeight: 800,
+ });
+ Fantom.runTask(() => {
+ reference.render(
+ ,
+ );
+ });
+ expect(shownB).toEqual(renderedOutput(reference));
+ Fantom.runTask(() => {
+ reference.render(
+ ,
+ );
+ });
+ expect(shownC).toEqual(renderedOutput(reference));
+
+ root.destroy();
+ reference.destroy();
+ });
+
+ it('does not clone a hidden subtree when its parent is laid out again', () => {
+ const root = Fantom.createRoot({viewportWidth: 400, viewportHeight: 800});
+ const headerRef = React.createRef>();
+ const hiddenLeafRef = React.createRef>();
+ const render = (headerHeight: number, hiddenRows: number) => {
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+
+
+
+ {Array.from({length: hiddenRows}, (_, i) => (
+
+ ))}
+
+ ,
+ );
+ });
+ };
+
+ render(50, 0);
+ const getLeafRevision =
+ Fantom.createShadowNodeRevisionGetter(hiddenLeafRef);
+ let revision = getLeafRevision();
+
+ // React commits that lay out the hidden node's parent again.
+ render(60, 0);
+ render(50, 0);
+ expect(getLeafRevision()).toBe(revision);
+
+ // Adding a node under the hidden node may clone the subtree once. Later
+ // commits don't clone it again.
+ render(50, 1);
+ revision = getLeafRevision();
+ render(60, 1);
+ render(50, 1);
+ expect(getLeafRevision()).toBe(revision);
+
+ // A commit that doesn't go through React.
+ Fantom.runTask(() => {
+ headerRef.current?.setNativeProps({style: {height: 70}});
+ });
+ expect(getLeafRevision()).toBe(revision);
+
+ root.destroy();
+ });
+});