Compare commits

...
Author SHA1 Message Date
Blake Friedman 10e5b03474 Infra: clean out android build folder + add timing
- Removes packages/react-native/ReactAndroid/build as part of the release
testing steps.
- Added timing information to the release testing clean step.

Changelog: [Internal]

Test: Ran locally
2024-12-06 15:15:44 +00:00
hyochan 3efbe33ce0 Add pointerEvents support to Text component (#48081)
Summary:
This PR adds `pointerEvents` to the `TextProps` type.

### Motivation:
The `pointerEvents` property is already supported in `Text` components internally, but it was missing from the TypeScript definitions. By adding it to `TextProps`, developers can now use this property with full type safety and without TypeScript errors.

This is a type-only change and does not introduce any functional modifications.

## Changelog:

[GENERAL] [ADDED] - Added `pointerEvents` to `TextProps` type.

Pull Request resolved: https://github.com/facebook/react-native/pull/48081

Test Plan:
As this is a type-only update:
- Verified that the `pointerEvents` property is now recognized when used with `Text` components in TypeScript projects.
- Ensured there are no runtime changes or regressions by testing existing `Text` components for expected behavior.

Reviewed By: cipolleschi

Differential Revision: D66753454

Pulled By: javache

fbshipit-source-id: c8f21b11daa6001a309b1d29fd6259101d11f5d2
2024-12-06 03:56:14 -08:00
Pieter De Baets 4134b1c60d Pass around parentTag instead of parentShadowView (#48062)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48062

We never need the full ShadowView representation of `parent` and this is significantly cheaper to construct and pass around.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D66656411

fbshipit-source-id: 0b20e04c6beb95c498350085ec06fd57d1c11237
2024-12-06 03:31:04 -08:00
Zeya Peng 87ec0965a2 Allow setting debugID on AnimatedValue and TimingAnimation (#48106)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48106

This could make it easier to locate and debug AnimatedValue and driver from native - so far on the native side of animated, the only way to identify an Animation driver or AnimatedNode is integer IDs and the type, which made it difficult to debug when surface gets complicated

Here I only enabled it for AnimatedValue and TimingAnimation, because
* TimingAnimation is most commonly used
* all the animation drivers (frames, spring, decay) can only drive Value type of AnimatedNode on the native side, so it's the primitive component of AnimatedNode

Changelog: [Internal]

Reviewed By: yungsters

Differential Revision: D66790298

fbshipit-source-id: ddd64a5728120f061aa902f25c93b1701617031b
2024-12-06 02:40:30 -08:00
Chi Tsai c6f12254d1 Add default getStringData/getPropNameIdData implementation (#47530)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47530

Adds the default implementation for `getStringData`/`getPropNameIdData`
for VMs that do not provide their own implementation

Changelog: [Internal]

Reviewed By: neildhar

Differential Revision: D65638889

fbshipit-source-id: 0a97569433c09ffafbd08fec5d9c9fbf5639b778
2024-12-06 01:35:18 -08:00
Christoph Purrer e9f279117e Allow to provide a custom ImageManager for cxx platform (#48109)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48109

[Changelog] [Internal] -  Allow to provide a custom ImageManager for cxx platform

This change allows target platforms to pass a platform specific or app specific ImageManager implementation

Reviewed By: javache

Differential Revision: D66788794

fbshipit-source-id: d7e99cae5de0a4c60047763dce368271dd191b9c
2024-12-05 19:00:39 -08:00
Rubén Norte 7a81fd7a8a Migrate all feature flags to pragmas (#48098)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48098

Changelog: [internal]

TSIA. No more manual feature flag overrides in Fantom tests :D

Reviewed By: sammy-SC

Differential Revision: D66760120

fbshipit-source-id: a0493d6ca57f4fdad33a0667e3af99ed0f0b66ca
2024-12-05 17:06:11 -08:00
Rubén Norte be9b076087 Add support for specifying feature flags in pragmas (#48097)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48097

Changelog: [internal]

As per title, this allows us to specify both common and JS-only feature flags for tests in the docblock as pragmas (in the same pragma separated by spaces, or in different pragmas). E.g.:

```
/**
 * fantom_flags commonTestFlag:true
 * fantom_flags jsOnlyTestFlag:true
 */
```

The feature flags are overridden automatically for us before the tests start.

Reviewed By: javache

Differential Revision: D66760121

fbshipit-source-id: 7e227e0035a170dab81b1e6ce39600a01a748867
2024-12-05 17:06:11 -08:00
Rubén Norte db70b791ba Add ReactNativeFeatureFlagsDynamicProvider to allow configuration in C++ using dynamic values (#48093)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48093

Changelog: [internal]

We're going to add support for specifying feature flags in Fantom tests in pragmas. E.g.:

```
/**
 * fantom_flags commonTestFlag:true
 */
```

Users will be able to specify any feature flags in their tests, so we need a way to pass that information from the test file to the runner, and the runner has to be able to apply this dynamic configuration.

Because the API is statically typed in C++, we need to define a method for every possible feature flag configurable through this API. We could do it in userland, but we'd have to manually add a method every time there was a new feature flag we wanted to support.

Instead of doing that, this introduces a new abstraction in the feature flag system that codegens it for you.

The API is basically:

```
  folly::dynamic values = folly::dynamic::object();

  values["commonTestFlag"] = true;

  ReactNativeFeatureFlags::override(
      std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(values));

  EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
```

Then we can use this abstraction in Fantom to pass all the configured flags as `folly::dynamic` through this API.

Reviewed By: javache

Differential Revision: D66760118

fbshipit-source-id: c32329e5ca76923c3e0b9c0eb1fe8c3268e1f57b
2024-12-05 17:06:11 -08:00
Rubén Norte da6d089305 Extract logic to get Fantom test config to a standalone module (2nd attempt) (#48119)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48119

Changelog: [internal]

This is a re-land of https://github.com/facebook/react-native/pull/48092

Reviewed By: rshest

Differential Revision: D66820309

fbshipit-source-id: 6b07edcca6988eeb014f6b51ec82296d451bee14
2024-12-05 17:06:11 -08:00
Rubén Norte 1243679fe2 Export existing Fantom tests (2nd attempt) (#48118)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48118

Changelog: [internal]

This is a re-land of https://github.com/facebook/react-native/pull/48085

Reviewed By: rshest

Differential Revision: D66820308

fbshipit-source-id: b0ccd4b52965988015422ebdb8cd1172d1f5e9db
2024-12-05 17:06:11 -08:00
Pieter De Baets 18ebea533d Convert com.facebook.react.modules.network.ReactCookieJarContainer to Kotlin (#48089)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48089

Planning to make some changes here for perf, but converting to Kotlin first

Changelog: [Android][Removed] Made ReactCookieJarContainer internal.

Reviewed By: tdn120

Differential Revision: D66724567

fbshipit-source-id: bf96f8df8a5c901b47c371c7ed16b7a81de22ee7
2024-12-05 16:29:37 -08:00
Pieter De Baets e750059d98 Convert com.facebook.react.modules.network.ForwardingCookieHandler to Kotlin (#48088)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48088

Planning to make some changes here for perf, but converting to Kotlin first

Changelog: [Internal]

Reviewed By: cortinico

Differential Revision: D66724321

fbshipit-source-id: dec7f7123abdcd5792b3d589269b40ad42b3d307
2024-12-05 16:29:37 -08:00
Richard Barnes e7b9d70e0a Remove unused-variable in ../xplat/js/react-native-github/packages/react-native/React/CxxLogUtils/RCTDefaultCxxLogFunction.mm +2 (#48105)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48105

LLVM-15 has a warning `-Wunused-variable` which we treat as an error because it's so often diagnostic of a code issue. Unused variables can compromise readability or, worse, performance.

This diff either (a) removes an unused variable and, possibly, it's associated code or (b) qualifies the variable with `[[maybe_unused]]`.

 - If you approve of this diff, please use the "Accept & Ship" button :-)

Changelog: [Internal]

Reviewed By: palmje

Differential Revision: D66777665

fbshipit-source-id: fadf71fd37c2b95f87419acf9d5a7765fe031905
2024-12-05 14:14:21 -08:00
Nicola Corti 13900d75b8 Remove replaceAll from RNTester sample code (#48099)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48099

This is currently breaking the Sample Module screen on RN-Tester. Let's remove it.

Changelog:
[Internal] [Changed] - Remove replaceAll from RNTester sample code

Reviewed By: cipolleschi

Differential Revision: D66764656

fbshipit-source-id: acd123374d23b37977d5506f70f29da7f5d6311f
2024-12-05 09:54:47 -08:00
Ramanpreet Nara 2f0977d8e4 Also report non-fatal non-warning errors (#48104)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48104

Just porting over the logic after D28815228.

Changelog: [Internal]

Reviewed By: mlord93

Differential Revision: D66563226

fbshipit-source-id: 41e21812dd0b2104fa66b970212f51bbb77d910b
2024-12-05 09:42:44 -08:00
Riccardo Cipolleschi 00d5caee99 Do not install CMake on Windows machine (#48122)
Summary:
GHA to build HermesC for windows are failing because the machines comes with a different CMake version already.
Let's try not to install Cmake and use the one provided by the machine.

## Changelog:
[Internal] -

Pull Request resolved: https://github.com/facebook/react-native/pull/48122

Test Plan: GHA {F1973187648}

Reviewed By: alanleedev

Differential Revision: D66825216

Pulled By: cipolleschi

fbshipit-source-id: 9a9376a5409e192195a6b6cc25b4d58cb47f15da
2024-12-05 09:19:58 -08:00
Nikita Rubilov ea84bc666c Revert D66599197: Delete tests migrated to Fantom and unnecessary mocks for FabricUIManager, DOM, etc.
Differential Revision:
D66599197

Original commit changeset: 33822588c217

Original Phabricator Diff: D66599197

fbshipit-source-id: 00891602920f84a04ea9eac32758d2af08f3d4c7
2024-12-05 06:46:15 -08:00
Nikita Rubilov ca908c0681 Revert D66702625: Export existing Fantom tests
Differential Revision:
D66702625

Original commit changeset: e136ea5ea42c

Original Phabricator Diff: D66702625

fbshipit-source-id: 43600daaf46e5c1522758d721b0d2f2c9abc7e25
2024-12-05 06:46:15 -08:00
Nikita Rubilov eee5d2ec3c Revert D66760119: Extract logic to get Fantom test config to a standalone module
Differential Revision:
D66760119

Original commit changeset: e955e8f59669

Original Phabricator Diff: D66760119

fbshipit-source-id: ef77a5cdc87efd81bbe503a2a943b1deef4a7e15
2024-12-05 06:46:15 -08:00
zhongwuzw efd57d681c Fabric: Post RCTInstanceDidLoadBundle notification after bundle loaded (#48082)
Summary:
Fixes https://github.com/facebook/react-native/issues/47949

## Changelog:

[IOS] [FIXED] - Fabric: Post RCTInstanceDidLoadBundle notification after bundle loaded

Pull Request resolved: https://github.com/facebook/react-native/pull/48082

Test Plan: Post RCTInstanceDidLoadBundle notification after bundle loaded

Reviewed By: philIip

Differential Revision: D66754060

Pulled By: cipolleschi

fbshipit-source-id: d30f0ed73e127936082e6f91e137b9b4013c6651
2024-12-05 06:00:34 -08:00
Rubén Norte bc0b5ca5df Extract logic to get Fantom test config to a standalone module (#48092)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48092

Changelog: [internal]

This is just in preparation to expand the scope of that function to include configuration for feature flags.

Reviewed By: javache

Differential Revision: D66760119

fbshipit-source-id: e955e8f596697ac6a0a87013bec3fc3e09caf19d
2024-12-05 05:18:08 -08:00
Rubén Norte f6aae38e52 Export existing Fantom tests (#48085)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48085

Changelog: [internal]

Reviewed By: rshest

Differential Revision: D66702625

fbshipit-source-id: e136ea5ea42c1e1942e4c22e65855e91ad96e3f8
2024-12-05 05:18:08 -08:00
Rubén Norte 2e7e065d09 Delete tests migrated to Fantom and unnecessary mocks for FabricUIManager, DOM, etc. (#48087)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48087

Changelog: [internal]

Now that we have Fantom tests for these unit tests that use mocks, we can remove the JS tests and the mocks :)

Reviewed By: sammy-SC

Differential Revision: D66599197

fbshipit-source-id: 33822588c2176ffe2f2631da56c671b299f8058d
2024-12-05 05:18:08 -08:00
Rubén Norte b487e65869 Add support for expect() .toBeLessThan and .toBeGreaterThan (#48086)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48086

Changelog: [internal]

Just adding a bit of coverage for the `expect` API adding `toBeLessThan` and `toBeGreaterThan`.

Reviewed By: sammy-SC

Differential Revision: D66753268

fbshipit-source-id: 6a26f558f985ccbb5eb0daacecd93759841149e9
2024-12-05 05:18:08 -08:00
Rubén Norte ff934cd249 Extract definitions for expect and mocks from generic test setup to standalone modules (#48083)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48083

Changelog: [internal]

Just a small refactor to have a better code organization for the testing runtime infra.

Reviewed By: sammy-SC

Differential Revision: D66753269

fbshipit-source-id: e68727fe45fabe0be3528e21d5a60cef3045c252
2024-12-05 05:18:08 -08:00
Rubén Norte 118c18f3bb Allow async functions in ReactNativeTester and add tests (#48063)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48063

Changelog: [internal]

This allows async functions to be passed to `runTask` (just a type change really) and adds tests for ReactNativeTester. Error handling isn't currently set up correctly, so those tests are disabled for now.

Reviewed By: sammy-SC

Differential Revision: D66698547

fbshipit-source-id: 41d1fccc80f90cdf764f6fa3d3d34365eeef8ec6
2024-12-05 05:18:08 -08:00
Wojciech Lewicki e7f943de2f fix: long not implemented on native side (#48017)
Summary:
Handling `long` values is not implemented in Writables. I added it on the native side.

## Changelog:

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

[ANDROID] [FIXED] - Support Long values in WritableMap and WritableArray

Pull Request resolved: https://github.com/facebook/react-native/pull/48017

Test Plan: Run https://github.com/WoLewicki/reproducer-react-native/tree/%40wolewicki/long-in-writeable-map and see it doesn't work without those changes.

Reviewed By: javache

Differential Revision: D66754194

Pulled By: cortinico

fbshipit-source-id: 7f8d4eb3c4069f890460525ddffdf9f4324550b0
2024-12-05 04:46:28 -08:00
jodeppo 8aac234ce2 Remove Trigger E2E Tests on Comment (#47923)
Summary:
This action is no longer necessary and we can remove it

## Changelog:

[Internal] [Fixed] - Use `refs/pulls` namespace in trigger action

Pull Request resolved: https://github.com/facebook/react-native/pull/47923

Reviewed By: NickGerleman

Differential Revision: D66578573

Pulled By: cortinico

fbshipit-source-id: 87cdbc1544873a2669e82c7763c78d18ff7881fd
2024-12-05 03:23:52 -08:00
Kacper Kafara f402ed17fa Fix handling removal of transitioning views (#47634)
Summary:
Related PR in `react-native-screens`:

* https://github.com/software-mansion/react-native-screens/pull/2495

Additional context:
   * [my detailed explanation of **one of the issues**](https://github.com/software-mansion/react-native-screens/pull/2495#issuecomment-2478915818)
   * [Android Developer: ViewGroup.startViewTransition docs](https://developer.android.com/reference/android/view/ViewGroup#startViewTransition(android.view.View))

### Background

On Android view groups can be marked as "transitioning" with a `ViewGroup.startViewTransition` call. This effectively ensures, that in case a view group is marked with this call and its children are removed, they will be still drawn until `endViewTransition` is not called.

This mechanism is implemented in Android by [keeping track of "transitioning" children in auxiliary `mTransitioningViews` array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7178). Then when such "transitioning" child is removed, [it is removed from children array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5595) but it's [parent-child relationship is not cleared](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5397) and it is still retained in the auxiliary array.

Having that established we can proceed with problem description.

### Problem

https://github.com/user-attachments/assets/d0356bf5-2f17-4b06-ba53-bfca659a1071

<details>
<summary>Full code</summary>

```javascript
import { NavigationContainer } from 'react-navigation/native';
import React from 'react';
import { createNativeStackNavigator } from 'react-navigation/native-stack';
import { enableScreens } from 'react-native-screens';
import {
  StyleSheet,
  Text,
  View,
  FlatList,
  Button,
  ViewProps,
  Image,
  FlatListProps,
  findNodeHandle,
} from 'react-native';

enableScreens(true);

function Item({ children, ...props }: ViewProps) {
  return (
    <View style={styles.item} {...props}>
      <Image source={require('../assets/trees.jpg')} style={styles.image} />
      <Text style={styles.text}>{children}</Text>
    </View>
  );
}

function Home({ navigation }: any) {
  return (
    <View style={styles.container}>
      <Button title="Go to List" onPress={() => navigation.navigate('List')} />
    </View>
  );
}

function ListScreenSimplified({secondVisible}: {secondVisible?: (visible: boolean) => void}) {
  const containerRef = React.useRef<View>(null);
  const innerViewRef = React.useRef<View>(null);
  const childViewRef = React.useRef<View>(null);

  React.useEffect(() => {
    if (containerRef.current != null) {
      const tag = findNodeHandle(containerRef.current);
      console.log(`Container has tag [${tag}]`);
    }
    if (innerViewRef.current != null) {
      const tag = findNodeHandle(innerViewRef.current);
      console.log(`InnerView has tag [${tag}]`);
    }
    if (childViewRef.current != null) {
      const tag = findNodeHandle(childViewRef.current);
      console.log(`ChildView has tag [${tag}]`);
    }
  }, [containerRef.current, innerViewRef.current, childViewRef.current]);

  return (
    <View
      ref={containerRef}
      style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }}
      removeClippedSubviews={false}>
      <View ref={innerViewRef} removeClippedSubviews style={{ height: '100%' }}>
        <View ref={childViewRef} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} removeClippedSubviews={false}>
          {secondVisible && (<Button title='Hide second' onPress={() => secondVisible(false)} />)}
        </View>
      </View>
    </View>
  );
}

function ParentFlatlist(props: Partial<FlatListProps<number>>) {
  return (
    <FlatList
      data={Array.from({ length: 30 }).fill(0) as number[]}
      renderItem={({ index }) => {
        if (index === 10) {
          return <NestedFlatlist key={index} />;
        } else if (index === 15) {
          return <ExtraNestedFlatlist key={index} />;
        } else if (index === 20) {
          return <NestedFlatlist key={index} horizontal />;
        } else if (index === 25) {
          return <ExtraNestedFlatlist key={index} horizontal />;
        } else {
          return <Item key={index}>List item {index + 1}</Item>;
        }
      }}
      {...props}
    />
  );
}

function NestedFlatlist(props: Partial<FlatListProps<number>>) {
  return (
    <FlatList
      style={[styles.nestedList, props.style]}
      data={Array.from({ length: 10 }).fill(0) as number[]}
      renderItem={({ index }) => (
        <Item key={'nested' + index}>Nested list item {index + 1}</Item>
      )}
      {...props}
    />
  );
}

function ExtraNestedFlatlist(props: Partial<FlatListProps<number>>) {
  return (
    <FlatList
      style={styles.nestedList}
      data={Array.from({ length: 10 }).fill(0) as number[]}
      renderItem={({ index }) =>
        index === 4 ? (
          <NestedFlatlist key={index} style={{ backgroundColor: '#d24729' }} />
        ) : (
          <Item key={'nested' + index}>Nested list item {index + 1}</Item>
        )
      }
      {...props}
    />
  );
}

const Stack = createNativeStackNavigator();

export default function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ animation: 'slide_from_right' }}>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="List" component={ListScreenSimplified}/>
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export function AppSimple(): React.JSX.Element {
  const [secondVisible, setSecondVisible] = React.useState(false);

  return (
    <View style={{ flex: 1, backgroundColor: 'lightsalmon' }}>
      {!secondVisible && (
        <View style={{ flex: 1, backgroundColor: 'lightblue' }} >
          <Button title='Show second' onPress={() => setSecondVisible(true)} />
        </View>
      )}
      {secondVisible && (
        <ListScreenSimplified secondVisible={setSecondVisible} />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  nestedList: {
    backgroundColor: '#FFA07A',
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 10,
    gap: 10,
  },
  text: {
    fontSize: 24,
    fontWeight: 'bold',
    color: 'black',
  },
  image: {
    width: 50,
    height: 50,
  },
});

```

</details>

Explanation (copied from [here](https://github.com/software-mansion/react-native-screens/pull/2495#issuecomment-2478915818)):

I've debugged this for a while now & I have good understanding of what's going on. This bug is caused by our usage of `startViewTransition` and its implications. We use it well, however React does not account for case that some view might be in transition. Error mechanism is as follows:

1. Let's have initially simple stack with two screens: "A, B". This is component rendered under "B":

```javascript
    <View //<-- ContainerView (CV)
      removeClippedSubviews={false}
      style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }}>
      <View removeClippedSubviews style={{ height: '100%' }}> // <--- IntermediateView (IV)
        <View removeClippedSubviews={false} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} /> // <--- ChildView (ChV)
      </View>
    </View>
```

2. We press the back button.
3. We're on Fabric, therefore subtree of B gets destroyed before B itself is unmounted -> in our commit hook we detect that the screen B will be unmounted & we mark every node under B as transitioning by calling `startViewTransition`.
4. React Mounting stage starts, view hierarchy is disassembled in bottom-up fashion (leafs first).
5. ReactViewGroupManager receives MountItem to detach ChV from IV.
6. A call to [`IV.removeView(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L58-L73), which effectively removes ChV from `IV.children`, ***HOWEVER*** it does not clear `ChV.parent`, meaning that after the call, `ChV.parent == IV`. This happens, due to view being marked as in-transition by our call to `startViewTransition`. If the view is not marked as in-transition this parent-child relationship is removed.
7. IV has `removeClippedSubviews` enabled, therefore a [call to `IV.removeViewWithSubviewsClippingEnabled(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L68). [This function](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java#L726-L744) does effectively two things:
    1. if the ChV has parent (interpretation: it has not yet been detached from parent), we compute it's index in `IV.children` (Android.ViewGroup's state) and remove it from the array,
    2. remove the ChV from `mAllChildren` array (this is state maintained by ReactViewGroup for purposes of implementing the "subview clipping" mechanism".

The crash happens in 7.1, because ChV has been removed from `IV.children` in step 6, but the parent-child relationship has not been broken up there. Under usual circumstances (this is my hypothesis now, yet unconfirmed) 7.1 does not execute, because `ChV.parent` is nulled in step no. 6.

### Rationale for `startViewTransition` usage

Transitions. On Fabric, when some subtree is unmounted, views in the subtree are unmounted in bottom-up order. This leads to uncomfortable situation, where our components (react-native-screens), who want to drive & manage transitions are notified that their children will be removed after the subtrees mounted in screen subviews are already disassembled. **If we start animation in this very moment we will have staggering effect of white flash** [(issue)](https://github.com/software-mansion/react-native-screens/issues/1685) (we animate just the screen with white background without it's children). This was not a problem on Paper, because the order of subtree disassembling was opposite - top-down. While we've managed to workaround this issue on Fabric using `MountingTransactionObserving` protocol on iOS and a commit hook on Android (we can inspect mutations in incoming transaction before it starts being applied) we still need to prevent view hierarchy from being disassembled in the middle of transition (on Paper this has also been less of an issue) - and this is where `startViewTransition` comes in. It allows us to draw views throughout transition after React Native removes them from HostTree model. On iOS we exchange subtree for its snapshot for transition time, however this approach isn't feasible on Android, because [snapshots do not capture shadows](https://stackoverflow.com/questions/42212600/android-screenshot-of-view-with-shadow).

### Possible solutions

[Android does not expose a method to verify whether a view is in transition](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7162) (it has `package` visibility), therefore we need to retrieve this information with some workaround. I see two posibilities:

* first approach would be to override `startViewTransition` & `endViewTransition` in ReactViewGroup and keep the state on whether the view is transitioning there,
* second possible approach would be as follows: we can check for "transitioning" view by checking whether a view has parent but is not it's parent child (this **should** be reliable),

Having information on whether the view is in transition or not, we can prevent multiple removals of the same view in every call site (currently only in `removeViewAt` if `parent.removeClippingSubviews == true`).

Another option would be to do just as this PR does: having in mind this "transitioning" state we can pass a flag to `removeViewWithSubviewClippingEnabled` and prevent duplicated removal from parent if we already know that this has been requested.

I can also add override of this method:

```java
  /*package*/ void removeViewWithSubviewClippingEnabled(View view) {
    this.removeViewWithSubviewClippingEnabled(view, false);
  }
```

to make this parameter optional.

## Changelog:

[ANDROID] [FIXED] - Handle removal of in-transition views.

Pull Request resolved: https://github.com/facebook/react-native/pull/47634

Test Plan: WIP WIP

Reviewed By: javache

Differential Revision: D66539065

Pulled By: tdn120

fbshipit-source-id: cf1add67000ebd1b5dfdb2048461a55deac10b16
2024-12-04 15:24:35 -08:00
Nicola Corti 734730df75 Re-introduce the deprecated constructor on ReactModuleInfo (#48090)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48090

This alleviates a breaking change on `ReactModuleInfo` constructor.
While the ctor was deprecated, we realized that there are more than 250 usages in OSS.
We'll need to properly communicate this removal before we do it.

Changelog:
[Android] [Fixed] - Re-introduce the deprecated constructor on ReactModuleInfo

Reviewed By: cipolleschi

Differential Revision: D66755541

fbshipit-source-id: 3673d8f2af278d55491cea89f1594d368513e3d8
2024-12-04 11:03:03 -08:00
Mateo Guzmán 50d0157f0c test(network): [android] ResponseUtil unit tests (#48075)
Summary:
Currently the class `ResponseUtil` is still in Java, I'm adding some unit tests so it is safer to migrate it to Kotlin.

## Changelog:

[INTERNAL] - `ResponseUtil` unit tests

Pull Request resolved: https://github.com/facebook/react-native/pull/48075

Test Plan:
```bash
yarn test-android
```

Reviewed By: cortinico

Differential Revision: D66727736

Pulled By: lunaleaps

fbshipit-source-id: 9c89c75905b4e0c9c4820a556245a07e135e0f17
2024-12-04 10:26:04 -08:00
Andrew Datsenko 2e444d2b40 Add ReactNativetester#getRenderedOutput() API (#47970)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47970

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D65617491

fbshipit-source-id: 49369b694a81b7dfb541c75d9e24b62fc141d980
2024-12-04 09:55:56 -08:00
Nick Gerleman 366270e742 Update requirements in README (#48079)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48079

We updated these in 0.76, and should update them in the README as well. https://reactnative.dev/blog/2024/10/23/release-0.76-new-architecture#updates-to-minimum-ios-and-android-sdk-requirements

Changelog: [Internal]

Reviewed By: lunaleaps, philIip

Differential Revision: D66735446

fbshipit-source-id: c9145bab14e4956ed070fb906dd1c1676905bfb6
2024-12-04 09:45:46 -08:00
Soe Lynn 469978f170 Fix Interop code for having unsync ViewManager between ModuleRegistry and BridgeProxy (#48069)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48069

Changelog [Internal]:
Fix Interop code for having unsync ViewManager between ModuleRegistry and BridgeProxy

Reviewed By: RSNara

Differential Revision: D66137400

fbshipit-source-id: ace3f60b6b972f17c7124ec33f0d3e8d035e966c
2024-12-04 09:40:16 -08:00
Rob Hogan 7ee7e69cdf Update changelog for v0.77.0-rc.1 (#48084)
Summary:
Changelog for v0.77.0-rc.1

https://github.com/facebook/react-native/compare/v0.77.0-rc.0...v0.77.0-rc.1

## Changelog:
[Internal]

Pull Request resolved: https://github.com/facebook/react-native/pull/48084

Reviewed By: cipolleschi

Differential Revision: D66753599

Pulled By: robhogan

fbshipit-source-id: 9a02df5cb7174270b4d293c48c6b0468a4042c87
2024-12-04 09:40:07 -08:00
zhongwuzw efcfe5dcd6 Fabric: Fixes insets not adjust when keyboard disappear (#47924)
Summary:
Fixes https://github.com/facebook/react-native/issues/47731 .

## Changelog:

[IOS] [FIXED] - Fabric: Fixes insets not adjust when keyboard disappear

Pull Request resolved: https://github.com/facebook/react-native/pull/47924

Test Plan: Demo in https://github.com/facebook/react-native/issues/47731

Reviewed By: blakef

Differential Revision: D66651865

Pulled By: cipolleschi

fbshipit-source-id: a75afbd1a7651f0c77022d913f910821c482fcf7
2024-12-04 07:49:12 -08:00
Rob Hogan b5b9e032c2 Fix Android JSC compatibility - replaceAll -> replace (#48076)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48076

JSC for Android does not implement `String.prototype.replaceAll`:

 {F1971791988}

https://github.com/facebook/react-native/pull/47466 introduced a use of it into runtime code, breaking JSC compatibility.

This.. replaces it.. with `replace`. Since the argument is already a regex with a `g` modifier, `replaceAll` wasn't necessary anyway.

Changelog:
[ANDROID][FIXED] Fix JSC by avoiding use of unavailable `str.replaceAll()`

Reviewed By: javache

Differential Revision: D66712312

fbshipit-source-id: 534b6db6834a2fda46ae8457437de3caa24f4eb0
2024-12-04 06:07:21 -08:00
Rubén Norte 4c62d46525 Remove legacy versions of the native methods for performance.mark and performance.measure (#48067)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48067

Changelog: [internal]

We don't need to keep these versions for backwards compatibility anymore.

Reviewed By: rshest

Differential Revision: D65423761

fbshipit-source-id: 59046a577c1de4aedb2593a12a45d9deb3bb4260
2024-12-04 04:38:54 -08:00
Phillip Pan 0217d7e19c have RCTInstance decorate non-app provided turbomodules with bridgeless APIs (#48053)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48053

Changelog: [Internal]

`getModuleInstanceFromClass:` is a delegate method intended to be implemented by the product layer to provide modules. if it is not implemented to return a module for a given key, `RCTTurboModuleManager` will simply call `new` on the TM class.

however, these two paths differentiate - for `getModuleInstanceFromClass:`, we will call `_attachBridgelessAPIsToModule:` which provides objects like surfacePresenter to the native module.

if we fallback to calling `new`, then this attachment does not happen, even if the app has already been migrated to bridgeless modules.

thus, the fix in the case is to lift the fallback into RCTInstance as well, and decorate the APIs onto the new fallback.

Reviewed By: cipolleschi

Differential Revision: D66675034

fbshipit-source-id: 1ab89a4006d05f744f5d42b5de786ccea4d4a55d
2024-12-03 22:23:04 -08:00
Joe Vilches 74f3ab7d40 Properly camelcase mainAxisownerSize in FlexLine (#48077)
Summary:
X-link: https://github.com/facebook/yoga/pull/1762

Pull Request resolved: https://github.com/facebook/react-native/pull/48077

OCD strikes again. Grepped this time to make sure we didn't miss any cases for this specific param name

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66715777

fbshipit-source-id: 3e881a15b3b2836a4a55b11d7ec621541b92a05d
2024-12-03 19:16:56 -08:00
Eli White 949d229b5f Apply enum changes to new codegen version (#48000)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48000

Adding this type to CompleteTypes

Changelog: [Internal]

Reviewed By: GijsWeterings

Differential Revision: D65305755

fbshipit-source-id: 962297ba21b3b88f0117631fb4192c111e903fc6
2024-12-03 16:41:36 -08:00
Vojtech Novak 52f09276cc fix hermes param handling in test-e2e-local.js (#48068)
Summary:
why: running `yarn test-e2e-local -t "RNTestProject" -p "Android" -h false -c $GITHUB_TOKEN` would actually build the app with Hermes even though it's specified as disabled.

This is because of the `if (argv.hermes == null)` condition whose body would not execute.
The condition was changed [recently](https://github.com/facebook/react-native/commit/f322dc7a84eb72370910f6933d0a4fa7780f49bc#diff-56f57bf0eac99b0fda1b2938aceb8d9b663db82c07cb405bd53a01c8689710ffR258).

Reason for `await` being used:

```
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ scripts/release-testing/test-e2e-local.js:303:32

Cannot get argv.hermes because property hermes is missing in Promise [1]. [prop-missing]

     scripts/release-testing/test-e2e-local.js
     300│     'reactNativeArchitectures=arm64-v8a',
     301│     'android/gradle.properties',
     302│   );
     303│   const hermesEnabled = (argv).hermes === true;
     304│
     305│   // Update gradle properties to set Hermes as false
     306│   if (!hermesEnabled) {

     flow-typed/npm/yargs_v17.x.x.js
 [1]  80│     argv: Argv | Promise<Argv>;
 ```

## Changelog:

[INTERNAL] [FIXED] - fix `hermes` param handling in `test-e2e-local.js`

Pull Request resolved: https://github.com/facebook/react-native/pull/48068

Test Plan: tested locally

Reviewed By: cipolleschi

Differential Revision: D66704263

Pulled By: robhogan

fbshipit-source-id: f05f23b95e67bd20025e0b3448df0d284fcb62da
2024-12-03 16:14:32 -08:00
Richard Barnes 5e7eb9f3a6 Revert D66143498
Summary:
This diff reverts D66143498
T209377282 Breaking tests on Twilight

Differential Revision: D66717950

fbshipit-source-id: 640592761fec29ed6e11a8b6faf441dd44685c42
2024-12-03 14:31:53 -08:00
Dmitry Rykun 725527885a Android: Initiate image prefetching on ImageShadowNode layout (#47932)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47932

This diff introduces a code path to trigger image prefetching from `ImageShadowNode::layout`.
Changelog: [Internal]

Reviewed By: javache

Differential Revision: D66454087

fbshipit-source-id: 17f5fed7d29c7d69cf76c28562898a81fac24044
2024-12-03 13:31:24 -08:00
Luna Wei d19f5f97b8 Fix naming convention for new rootThreshold related APIs and add Fantom tests (#48071)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48071

Changelog: [Internal]

- Write fantom test for rn_rootThreshold given current implementation of IntersectionObserver
- Rename `rn_rootThreshold` to `rnRootThreshold`
- Rename `rn_intersectionRootRatio` to `rnRootIntersectionRatio`
- Rename `rootThresholds` on observer to `rnRootThresholds`

Reviewed By: rubennorte

Differential Revision: D66464509

fbshipit-source-id: 8ed66afa54bab99a28625ebe6f227d59d0bd7389
2024-12-03 13:29:51 -08:00
Luna Wei 9aa21b5e87 Return intersection rect (#48070)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48070

Changelog: [General][Changed]

Return the clipped `intersectionRect` in IntersectionObserverEntry regardless of whether the observer `isIntersecting` or not. This addresses a deviance from the [web spec](https://www.w3.org/TR/intersection-observer/?fbclid=IwZXh0bgNhZW0CMTEAAR1XaWZim1ij0N1p07aCM__SYerXhu88UTDZRFCZEvRhQW2crRMwEvfwAdQ_aem_zH8WjTh0VFjEeORG76rcew#intersection-observer-entry)

Reviewed By: rubennorte

Differential Revision: D66516179

fbshipit-source-id: fdc766f0e6fc0a899b1b11547a2aa06010b8d010
2024-12-03 13:29:51 -08:00
Pieter De Baets 9800c8e47e Improve differentiator logging (#48061)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48061

Improve consistency of debug logs in differentiator.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D66654292

fbshipit-source-id: f4accdea184b932f94359c893e6de59f8139ca22
2024-12-03 10:17:02 -08:00
Pieter De Baets 34901d4861 Fix differentiator emitting updates with incorrect parentTag (#48055)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48055

Address the test-case identified in D66557919, where Differentiator could emit updates for views referencing an incorrect parentTag.

The longer-term fix here is to avoid emitting any updates for nodes which are being reparented, but that requires bigger changes, including to the LayoutAnimation system. As a short-term patch, we're passing through an explicit `parentShadowViewForUpdate` which will be used as the current parent for update purposes.

 {F1971278019}

Changelog: [Android][Fixed] Fix Fabric mutations sometimes triggering a `getViewState` crash when referencing an invalid parentTag.

Reviewed By: rubennorte

Differential Revision: D66654293

fbshipit-source-id: cd5b3e577ad1eede1b6dea834582ac6d750cbb81
2024-12-03 10:17:02 -08:00
Fabrizio Cucci 21c9491926 Migrate package com.facebook.react.uimanager.RootView to Kotlin (#47701)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47701

As per title.

Changelog:
[Android][Breaking] Convert RootView to Kotlin

Reviewed By: cortinico

Differential Revision: D66159881

fbshipit-source-id: 082881a03946088293dde3c085e1d1882bac96be
2024-12-03 09:59:48 -08:00
Blake Friedman 9df20d414e Log out which workflow artifact we're using (#48046)
Summary:
Make it clearer to the release crew to avoid using an outdated artifact
for testing a release.

{F1971030533}

Changelog: [Internal]

Pull Request resolved: https://github.com/facebook/react-native/pull/48046

Test Plan:
```
yarn test-e2e-local -t "RNTester" -p "Android" -h true -c $GITHUB_TOKEN
```

Reviewed By: robhogan

Differential Revision: D66657082

Pulled By: blakef

fbshipit-source-id: 225128690c180bee7a3d28fdcc7f8c9885a37f0d
2024-12-03 09:34:48 -08:00
Alex Hunt d6f286a4a0 Add description to React-jsinspector.podspec, refactoring (#48066)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48066

Minor edits to this build spec to align with other packages in `ReactCommon/`.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D66700351

fbshipit-source-id: 47942c27d6154b78c165508447a3056f1354f5c3
2024-12-03 08:44:48 -08:00
135 changed files with 8156 additions and 1141 deletions
@@ -43,9 +43,6 @@ runs:
shell: powershell
run: |
if (-not(Test-Path -Path $Env:HERMES_WS_DIR\win64-bin\hermesc.exe)) {
choco install --no-progress cmake --version 3.14.7 --allow-downgrade
if (-not $?) { throw "Failed to install CMake" }
cd $Env:HERMES_WS_DIR\icu
# If Invoke-WebRequest shows a progress bar, it will fail with
# Win32 internal error "Access is denied" 0x5 occurred [...]
@@ -1,45 +0,0 @@
name: Trigger E2E Tests on Comment
# This workflow is used to automatically trigger E2E tests when a comment is made
# containing the text "/run-e2e-tests".
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
trigger-e2e-tests:
name: Trigger E2E Tests
runs-on: ubuntu-latest
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/test-e2e')
steps:
# This is needed because of https://github.com/actions/runner-images/issues/6283
# TL;DR: brew is not in the PATH anymore.
- name: Setup Homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Install jq
run: brew install jq
- name: Run E2E Tests
run: |
# Github does not provide the branch of a PR when a comment on a PR is made
# So, given the issue number, which is the PR number, we can retrieve the branch with
# a quick API call
echo "Retrieving branch"
BRANCH=$(curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/facebook/react-native/pulls/$PR_NUMBER | jq -r '.head.ref')
echo "Trigger Test All workflow for branch $BRANCH"
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/facebook/react-native/actions/workflows/test-all.yml/dispatches \
-d "{\"ref\": \"$BRANCH\", \"inputs\": {\"run-e2e-tests\": \"true\"}}"
env:
GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## v0.77.0-rc.1
### Fixed
- Replace Object.hasOwn usages to fix Animated on JSC ([e996b3f346](https://github.com/facebook/react-native/commit/e996b3f346462a394012a722ce19990cdf9c3d9a) by [@robhogan](https://github.com/robhogan))
- Remove non compliant `filename*` attribute in a FormData `content-disposition` header ([f791fb9e66](https://github.com/facebook/react-native/commit/f791fb9e660fe15bccf55029045c48f4bbcbc5cb) by [@foyarash](https://github.com/foyarash))
- Fix "punycode is deprecated" warning by replacing `node-fetch` with native `fetch` ([881d8a720f](https://github.com/facebook/react-native/commit/881d8a720fb24241d7b2127273ca6116833bf176) by [@jbroma](https://github.com/jbroma))
#### Android specific
- Reverted removal of TurboReactPackage ([70a957452c](https://github.com/facebook/react-native/commit/70a957452c438a74787f4f752b2c274360cb2edd) by [@javache](https://github.com/javache))
- Fix IOException in `BuildCodegenCLITask` ([9147b0753a](https://github.com/facebook/react-native/commit/9147b0753a6c3afb2480b079f91614cd7189a28a) by [@vonovak](https://github.com/vonovak))
## v0.77.0-rc.0
### Breaking
+1 -1
View File
@@ -67,7 +67,7 @@ React Native is developed and supported by many companies and individual core co
## 📋 Requirements
React Native apps may target iOS 13.4 and Android 6.0 (API 23) or newer. You may use Windows, macOS, or Linux as your development operating system, though building and running iOS apps is limited to macOS. Tools like [Expo](https://expo.dev) can be used to work around this.
React Native apps may target iOS 15.1 and Android 7.0 (API 24) or newer. You may use Windows, macOS, or Linux as your development operating system, though building and running iOS apps is limited to macOS. Tools like [Expo](https://expo.dev) can be used to work around this.
## 🎉 Building your first React Native app
@@ -9,12 +9,18 @@
* @oncall react_native
*/
import type {FantomTestConfigJsOnlyFeatureFlags} from './getFantomTestConfig';
module.exports = function entrypointTemplate({
testPath,
setupModulePath,
featureFlagsModulePath,
featureFlags,
}: {
testPath: string,
setupModulePath: string,
featureFlagsModulePath: string,
featureFlags: FantomTestConfigJsOnlyFeatureFlags,
}): string {
return `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -29,6 +35,17 @@ module.exports = function entrypointTemplate({
*/
import {registerTest} from '${setupModulePath}';
${
Object.keys(featureFlags).length > 0
? `import * as ReactNativeFeatureFlags from '${featureFlagsModulePath}';
ReactNativeFeatureFlags.override({
${Object.entries(featureFlags)
.map(([name, value]) => ` ${name}: () => ${JSON.stringify(value)},`)
.join('\n')}
});`
: ''
}
registerTest(() => require('${testPath}'));
`;
@@ -0,0 +1,166 @@
/**
* 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
* @oncall react_native
*/
import ReactNativeFeatureFlags from '../../../packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config';
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {extract, parse} from 'jest-docblock';
type CommonFeatureFlags = (typeof ReactNativeFeatureFlags)['common'];
type JsOnlyFeatureFlags = (typeof ReactNativeFeatureFlags)['jsOnly'];
type DocblockPragmas = {[key: string]: string | string[]};
export type FantomTestConfigMode = 'dev' | 'opt';
export type FantomTestConfigCommonFeatureFlags = Partial<{
[key in keyof CommonFeatureFlags]: CommonFeatureFlags[key]['defaultValue'],
}>;
export type FantomTestConfigJsOnlyFeatureFlags = Partial<{
[key in keyof JsOnlyFeatureFlags]: JsOnlyFeatureFlags[key]['defaultValue'],
}>;
export type FantomTestConfig = {
mode: FantomTestConfigMode,
flags: {
common: FantomTestConfigCommonFeatureFlags,
jsOnly: FantomTestConfigJsOnlyFeatureFlags,
},
};
const DEFAULT_MODE: FantomTestConfigMode = 'dev';
const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
/**
* Extracts the Fantom configuration from the test file, specified as part of
* the docblock comment. E.g.:
*
* ```
* /**
* * @flow strict-local
* * @fantom_mode opt
* * @fantom_flags commonTestFlag:true
* * @fantom_flags jsOnlyTestFlag:true
* *
* ```
*
* The supported options are:
* - `fantom_mode`: specifies the level of optimization to compile the test
* with. Valid values are `dev` and `opt`.
* - `fantom_flags`: specifies the configuration for common and JS-only feature
* flags. They can be specified in the same pragma or in different ones, and
* the format is `<flag_name>:<value>`.
*/
export default function getFantomTestConfig(
testPath: string,
): FantomTestConfig {
const docblock = extract(fs.readFileSync(testPath, 'utf8'));
const pragmas = parse(docblock) as DocblockPragmas;
const config: FantomTestConfig = {
mode: DEFAULT_MODE,
flags: {
common: {},
jsOnly: {},
},
};
const maybeMode = pragmas.fantom_mode;
if (maybeMode != null) {
if (Array.isArray(maybeMode)) {
throw new Error('Expected a single value for @fantom_mode');
}
const mode = maybeMode;
if (mode === 'dev' || mode === 'opt') {
config.mode = mode;
} else {
throw new Error(`Invalid Fantom mode: ${mode}`);
}
}
const maybeRawFlagConfig = pragmas.fantom_flags;
if (maybeRawFlagConfig != null) {
const rawFlagConfigs = (
Array.isArray(maybeRawFlagConfig)
? maybeRawFlagConfig
: [maybeRawFlagConfig]
).flatMap(value => value.split(/\s+/g));
for (const rawFlagConfig of rawFlagConfigs) {
const matches = FANTOM_FLAG_FORMAT.exec(rawFlagConfig);
if (matches == null) {
throw new Error(
`Invalid format for Fantom feature flag: ${rawFlagConfig}. Expected <flag_name>:<value>`,
);
}
const [, name, rawValue] = matches;
if (ReactNativeFeatureFlags.common[name]) {
const flagConfig = ReactNativeFeatureFlags.common[name];
const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue);
config.flags.common[name] = value;
} else if (ReactNativeFeatureFlags.jsOnly[name]) {
const flagConfig = ReactNativeFeatureFlags.jsOnly[name];
const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue);
config.flags.jsOnly[name] = value;
} else {
const validKeys = Object.keys(ReactNativeFeatureFlags.common)
.concat(Object.keys(ReactNativeFeatureFlags.jsOnly))
.join(', ');
throw new Error(
`Invalid Fantom feature flag: ${name}. Valid flags are: ${validKeys}`,
);
}
}
}
return config;
}
function parseFeatureFlagValue<T: boolean | number | string>(
defaultValue: T,
value: string,
): T {
switch (typeof defaultValue) {
case 'boolean':
if (value === 'true') {
// $FlowExpectedError[incompatible-return] at this point we know T is a boolean
return true;
} else if (value === 'false') {
// $FlowExpectedError[incompatible-return] at this point we know T is a boolean
return false;
} else {
throw new Error(`Invalid value for boolean flag: ${value}`);
}
case 'number':
const parsed = Number(value);
if (Number.isNaN(parsed)) {
throw new Error(`Invalid value for number flag: ${value}`);
}
// $FlowExpectedError[incompatible-return] at this point we know T is a number
return parsed;
case 'string':
// $FlowExpectedError[incompatible-return] at this point we know T is a string
return value;
default:
throw new Error(`Unsupported feature flag type: ${typeof defaultValue}`);
}
}
+9 -1
View File
@@ -12,10 +12,10 @@
import type {TestSuiteResult} from '../runtime/setup';
import entrypointTemplate from './entrypoint-template';
import getFantomTestConfig from './getFantomTestConfig';
import {
getBuckModeForPlatform,
getDebugInfoFromCommandResult,
getFantomTestConfig,
getShortHash,
runBuck2,
symbolicateStackTrace,
@@ -104,10 +104,16 @@ module.exports = async function runTest(
});
const setupModulePath = path.resolve(__dirname, '../runtime/setup.js');
const featureFlagsModulePath = path.resolve(
__dirname,
'../../../packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js',
);
const entrypointContents = entrypointTemplate({
testPath: `${path.relative(BUILD_OUTPUT_PATH, testPath)}`,
setupModulePath: `${path.relative(BUILD_OUTPUT_PATH, setupModulePath)}`,
featureFlagsModulePath: `${path.relative(BUILD_OUTPUT_PATH, featureFlagsModulePath)}`,
featureFlags: testConfig.flags.jsOnly,
});
const entrypointPath = path.join(
@@ -151,6 +157,8 @@ module.exports = async function runTest(
'--',
'--bundlePath',
testBundlePath,
'--featureFlags',
JSON.stringify(testConfig.flags.common),
]);
if (rnTesterCommandResult.status !== 0) {
-50
View File
@@ -12,60 +12,10 @@
import {spawnSync} from 'child_process';
import crypto from 'crypto';
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {extract, parse} from 'jest-docblock';
import os from 'os';
// $FlowExpectedError[untyped-import]
import {SourceMapConsumer} from 'source-map';
type DocblockPragmas = {[key: string]: string | string[]};
type FantomTestMode = 'dev' | 'opt';
type FantomTestConfig = {
mode: FantomTestMode,
};
const DEFAULT_MODE: FantomTestMode = 'dev';
/**
* Extracts the Fantom configuration from the test file, specified as part of
* the docblock comment. E.g.:
*
* ```
* /**
* * @flow strict-local
* * @fantom mode:opt
* *
* ```
*
* So far the only supported option is `mode`, which can be 'dev' or 'opt'.
*/
export function getFantomTestConfig(testPath: string): FantomTestConfig {
const docblock = extract(fs.readFileSync(testPath, 'utf8'));
const pragmas = parse(docblock) as DocblockPragmas;
const config = {
mode: DEFAULT_MODE,
};
const maybeMode = pragmas.fantom_mode;
if (maybeMode != null) {
if (Array.isArray(maybeMode)) {
throw new Error('Expected a single value for @fantom_mode');
}
const mode = maybeMode;
if (mode === 'dev' || mode === 'opt') {
config.mode = mode;
} else {
throw new Error(`Invalid Fantom mode: ${mode}`);
}
}
return config;
}
export function getBuckModeForPlatform(enableRelease: boolean = false): string {
const mode = enableRelease ? 'opt' : 'dev';
+277
View File
@@ -0,0 +1,277 @@
/**
* 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
* @oncall react_native
*/
import {ensureMockFunction} from './mocks';
import deepEqual from 'deep-equal';
import {diff} from 'jest-diff';
class ErrorWithCustomBlame extends Error {
// Initially 5 to ignore all the frames from Babel helpers to instantiate this
// custom error class.
#ignoredFrameCount: number = 5;
#cachedProcessedStack: ?string;
#customStack: ?string;
blameToPreviousFrame(): this {
this.#cachedProcessedStack = null;
this.#ignoredFrameCount++;
return this;
}
// $FlowExpectedError[unsafe-getters-setters]
get stack(): string {
if (this.#cachedProcessedStack == null) {
const originalStack = this.#customStack ?? super.stack;
const lines = originalStack.split('\n');
const index = lines.findIndex(line =>
/at (.*) \((.*):(\d+):(\d+)\)/.test(line),
);
lines.splice(index > -1 ? index : 1, this.#ignoredFrameCount);
this.#cachedProcessedStack = lines.join('\n');
}
return this.#cachedProcessedStack;
}
// $FlowExpectedError[unsafe-getters-setters]
set stack(value: string) {
this.#cachedProcessedStack = null;
this.#customStack = value;
}
static fromError(error: Error): ErrorWithCustomBlame {
const errorWithCustomBlame = new ErrorWithCustomBlame(error.message);
// In this case we're inheriting the error and we don't know if the stack
// contains helpers that we need to ignore.
errorWithCustomBlame.#ignoredFrameCount = 0;
errorWithCustomBlame.stack = error.stack;
return errorWithCustomBlame;
}
}
class Expect {
#received: mixed;
#isNot: boolean = false;
constructor(received: mixed) {
this.#received = received;
}
// $FlowExpectedError[unsafe-getters-setters]
get not(): this {
this.#isNot = !this.#isNot;
return this;
}
toEqual(expected: mixed): void {
const pass = deepEqual(this.#received, expected, {strict: true});
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected${this.#maybeNotLabel()} to equal:\n${
diff(expected, this.#received, {
contextLines: 1,
expand: false,
omitAnnotationLines: true,
}) ?? 'Failed to compare outputs'
}`,
).blameToPreviousFrame();
}
}
toBe(expected: mixed): void {
const pass = this.#received === expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected${this.#maybeNotLabel()} ${String(expected)} but received ${String(this.#received)}.`,
).blameToPreviousFrame();
}
}
toBeInstanceOf(expected: Class<mixed>): void {
const pass = this.#received instanceof expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`expected ${String(this.#received)}${this.#maybeNotLabel()} to be an instance of ${String(expected)}`,
).blameToPreviousFrame();
}
}
toBeCloseTo(expected: number, precision: number = 2): void {
const pass =
Math.abs(expected - Number(this.#received)) < Math.pow(10, -precision);
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be close to ${expected}`,
).blameToPreviousFrame();
}
}
toBeNull(): void {
const pass = this.#received == null;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be null`,
).blameToPreviousFrame();
}
}
toThrow(expected?: string): void {
if (expected != null && typeof expected !== 'string') {
throw new ErrorWithCustomBlame(
'toThrow() implementation only accepts strings as arguments.',
).blameToPreviousFrame();
}
let pass = false;
try {
// $FlowExpectedError[not-a-function]
this.#received();
} catch (error) {
pass = expected != null ? error.message === expected : true;
}
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to throw`,
).blameToPreviousFrame();
}
}
toHaveBeenCalled(): void {
const mock = this.#requireMock();
const pass = mock.calls.length > 0;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called, but it was${this.#isNot ? '' : "n't"}`,
).blameToPreviousFrame();
}
}
toHaveBeenCalledTimes(times: number): void {
const mock = this.#requireMock();
const pass = mock.calls.length === times;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called ${times} times, but it was called ${mock.calls.length} times`,
).blameToPreviousFrame();
}
}
toBeGreaterThan(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received > expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be greater than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
toBeGreaterThanOrEqual(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received >= expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be greater than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
toBeLessThan(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received < expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be less than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
toBeLessThanOrEqual(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received <= expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be less than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
#isExpectedResult(pass: boolean): boolean {
return this.#isNot ? !pass : pass;
}
#maybeNotLabel(): string {
return this.#isNot ? ' not' : '';
}
#requireMock(): JestMockFn<Array<mixed>, mixed>['mock'] {
try {
return ensureMockFunction(this.#received).mock;
} catch (error) {
const errorWithCustomBlame = ErrorWithCustomBlame.fromError(error);
errorWithCustomBlame.message = `Expected ${String(this.#received)} to be a mock function, but it wasn't`;
errorWithCustomBlame
.blameToPreviousFrame() // ignore `ensureMockFunction`
.blameToPreviousFrame() // ignore `requireMock`
.blameToPreviousFrame(); // ignore `expect().[method]`
throw errorWithCustomBlame;
}
}
}
const expect: mixed => Expect = (received: mixed) => new Expect(received);
export default expect;
+83
View File
@@ -0,0 +1,83 @@
/**
* 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
* @oncall react_native
*/
export const MOCK_FN_TAG: symbol = Symbol('mock function');
// The type is defined this way because if we get a mixed value, we return
// a generic mock function, and if we get a typed function, we get a typed mock.
export const ensureMockFunction: (<TArgs: Array<mixed>, TReturn>(
fn: (...TArgs) => TReturn,
) => JestMockFn<TArgs, TReturn>) &
((fn: mixed) => JestMockFn<Array<mixed>, mixed>) = fn => {
// $FlowExpectedError[invalid-computed-prop]
// $FlowExpectedError[incompatible-use]
if (typeof fn !== 'function' || !fn[MOCK_FN_TAG]) {
throw new Error(
`Expected ${String(fn)} to be a mock function, but it wasn't`,
);
}
// $FlowExpectedError[incompatible-type]
// $FlowExpectedError[prop-missing]
return fn;
};
export function createMockFunction<TArgs: Array<mixed>, TReturn>(
initialImplementation?: (...TArgs) => TReturn,
): JestMockFn<TArgs, TReturn> {
let implementation: ?(...TArgs) => TReturn = initialImplementation;
const mock: JestMockFn<TArgs, TReturn>['mock'] = {
calls: [],
// $FlowExpectedError[incompatible-type]
lastCall: undefined,
instances: [],
contexts: [],
results: [],
};
const mockFunction = function (this: mixed, ...args: TArgs): TReturn {
let result: JestMockFn<TArgs, TReturn>['mock']['results'][number] = {
isThrow: false,
// $FlowExpectedError[incompatible-type]
value: undefined,
};
if (implementation != null) {
try {
result.value = implementation.apply(this, args);
} catch (error) {
result.isThrow = true;
result.value = error;
}
}
mock.calls.push(args);
mock.lastCall = args;
// $FlowExpectedError[incompatible-call]
mock.instances.push(new.target ? this : undefined);
mock.contexts.push(this);
mock.results.push(result);
if (result.isThrow) {
throw result.value;
}
return result.value;
};
mockFunction.mock = mock;
// $FlowExpectedError[invalid-computed-prop]
mockFunction[MOCK_FN_TAG] = true;
// $FlowExpectedError[prop-missing]
return mockFunction;
}
+3 -258
View File
@@ -9,7 +9,8 @@
* @oncall react_native
*/
import deepEqual from 'deep-equal';
import expect from './expect';
import {createMockFunction} from './mocks';
import nullthrows from 'nullthrows';
export type TestCaseResult = {
@@ -119,263 +120,7 @@ global.jest = {
fn: createMockFunction,
};
const MOCK_FN_TAG = Symbol('mock function');
function createMockFunction<TArgs: $ReadOnlyArray<mixed>, TReturn>(
initialImplementation?: (...TArgs) => TReturn,
): JestMockFn<TArgs, TReturn> {
let implementation: ?(...TArgs) => TReturn = initialImplementation;
const mock: JestMockFn<TArgs, TReturn>['mock'] = {
calls: [],
// $FlowExpectedError[incompatible-type]
lastCall: undefined,
instances: [],
contexts: [],
results: [],
};
const mockFunction = function (this: mixed, ...args: TArgs): TReturn {
let result: JestMockFn<TArgs, TReturn>['mock']['results'][number] = {
isThrow: false,
// $FlowExpectedError[incompatible-type]
value: undefined,
};
if (implementation != null) {
try {
result.value = implementation.apply(this, args);
} catch (error) {
result.isThrow = true;
result.value = error;
}
}
mock.calls.push(args);
mock.lastCall = args;
// $FlowExpectedError[incompatible-call]
mock.instances.push(new.target ? this : undefined);
mock.contexts.push(this);
mock.results.push(result);
if (result.isThrow) {
throw result.value;
}
return result.value;
};
mockFunction.mock = mock;
// $FlowExpectedError[invalid-computed-prop]
mockFunction[MOCK_FN_TAG] = true;
// $FlowExpectedError[prop-missing]
return mockFunction;
}
// flowlint unsafe-getters-setters:off
class ErrorWithCustomBlame extends Error {
// Initially 5 to ignore all the frames from Babel helpers to instantiate this
// custom error class.
#ignoredFrameCount: number = 5;
#cachedProcessedStack: ?string;
blameToPreviousFrame(): this {
this.#ignoredFrameCount++;
return this;
}
get stack(): string {
if (this.#cachedProcessedStack == null) {
const originalStack = super.stack;
if (originalStack == null) {
this.#cachedProcessedStack = originalStack;
} else {
const lines = originalStack.split('\n');
const index = lines.findIndex(line =>
/at (.*) \((.*):(\d+):(\d+)\)/.test(line),
);
lines.splice(index > -1 ? index : 1, this.#ignoredFrameCount);
this.#cachedProcessedStack = lines.join('\n');
}
}
return this.#cachedProcessedStack;
}
set stack(value: string) {
// no-op
}
}
class Expect {
#received: mixed;
#isNot: boolean = false;
constructor(received: mixed) {
this.#received = received;
}
get not(): this {
this.#isNot = !this.#isNot;
return this;
}
toEqual(expected: mixed): void {
const pass = deepEqual(this.#received, expected, {strict: true});
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected${this.#maybeNotLabel()} to equal ${String(expected)} but received ${String(this.#received)}.`,
).blameToPreviousFrame();
}
}
toBe(expected: mixed): void {
const pass = this.#received === expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected${this.#maybeNotLabel()} ${String(expected)} but received ${String(this.#received)}.`,
).blameToPreviousFrame();
}
}
toBeInstanceOf(expected: Class<mixed>): void {
const pass = this.#received instanceof expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`expected ${String(this.#received)}${this.#maybeNotLabel()} to be an instance of ${String(expected)}`,
).blameToPreviousFrame();
}
}
toBeCloseTo(expected: number, precision: number = 2): void {
const pass =
Math.abs(expected - Number(this.#received)) < Math.pow(10, -precision);
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be close to ${expected}`,
).blameToPreviousFrame();
}
}
toBeNull(): void {
const pass = this.#received == null;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be null`,
).blameToPreviousFrame();
}
}
toThrow(expected?: string): void {
if (expected != null && typeof expected !== 'string') {
throw new ErrorWithCustomBlame(
'toThrow() implementation only accepts strings as arguments.',
).blameToPreviousFrame();
}
let pass = false;
try {
// $FlowExpectedError[not-a-function]
this.#received();
} catch (error) {
pass = expected != null ? error.message === expected : true;
}
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to throw`,
).blameToPreviousFrame();
}
}
toHaveBeenCalled(): void {
const mock = this.#requireMock();
const pass = mock.calls.length > 0;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called, but it was${this.#isNot ? '' : "n't"}`,
).blameToPreviousFrame();
}
}
toHaveBeenCalledTimes(times: number): void {
const mock = this.#requireMock();
const pass = mock.calls.length === times;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to have been called ${times} times, but it was called ${mock.calls.length} times`,
).blameToPreviousFrame();
}
}
toBeGreaterThanOrEqual(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received >= expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be greater than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
toBeLessThanOrEqual(expected: number): void {
if (typeof this.#received !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a number but it was a ${typeof this.#received}`,
).blameToPreviousFrame();
}
if (typeof expected !== 'number') {
throw new ErrorWithCustomBlame(
`Expected ${String(expected)} to be a number but it was a ${typeof expected}`,
).blameToPreviousFrame();
}
const pass = this.#received <= expected;
if (!this.#isExpectedResult(pass)) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)}${this.#maybeNotLabel()} to be less than or equal to ${expected}`,
).blameToPreviousFrame();
}
}
#isExpectedResult(pass: boolean): boolean {
return this.#isNot ? !pass : pass;
}
#maybeNotLabel(): string {
return this.#isNot ? ' not' : '';
}
#requireMock(): JestMockFn<$ReadOnlyArray<mixed>, mixed>['mock'] {
// $FlowExpectedError[incompatible-use]
if (!this.#received?.[MOCK_FN_TAG]) {
throw new ErrorWithCustomBlame(
`Expected ${String(this.#received)} to be a mock function, but it wasn't`,
)
.blameToPreviousFrame()
.blameToPreviousFrame();
}
// $FlowExpectedError[incompatible-use]
return this.#received.mock;
}
}
global.expect = (received: mixed) => new Expect(received);
global.expect = expect;
function runWithGuard(fn: () => void) {
try {
+1 -1
View File
@@ -559,7 +559,7 @@ if (global.nativeLoggingHook) {
let originalConsoleError = console.error;
console.reportErrorsAsExceptions = true;
function stringifySafe(arg) {
return inspect(arg, {depth: 10}).replaceAll(/\n\s*/g, ' ');
return inspect(arg, {depth: 10}).replace(/\n\s*/g, ' ');
}
console.error = function (...args) {
originalConsoleError.apply(this, args);
+1
View File
@@ -430,4 +430,5 @@ export type CompleteTypeAnnotation =
| NativeModuleFunctionTypeAnnotation
| NullableTypeAnnotation<NativeModuleTypeAnnotation>
| EventEmitterTypeAnnotation
| NativeModuleEnumDeclarationWithMembers
| UnsafeAnyTypeAnnotation;
@@ -26,6 +26,7 @@ export type AnimationConfig = $ReadOnly<{
onComplete?: ?EndCallback,
iterations?: number,
isLooping?: boolean,
debugID?: ?string,
...
}>;
@@ -43,6 +44,7 @@ export default class Animation {
__isInteraction: boolean;
__isLooping: ?boolean;
__iterations: number;
__debugID: ?string;
constructor(config: AnimationConfig) {
this.#useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
@@ -51,6 +53,9 @@ export default class Animation {
this.__isInteraction = config.isInteraction ?? !this.#useNativeDriver;
this.__isLooping = config.isLooping;
this.__iterations = config.iterations ?? 1;
if (__DEV__) {
this.__debugID = config.debugID;
}
}
start(
@@ -99,6 +99,7 @@ export default class TimingAnimation extends Animation {
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
debugID: __DEV__ ? this.__debugID : undefined,
};
}
@@ -197,4 +197,6 @@ export default class AnimatedNode {
toJSON(): mixed {
return this.__getValue();
}
__debugID: ?string = undefined;
}
@@ -22,6 +22,7 @@ import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedValueConfig = $ReadOnly<{
useNativeDriver: boolean,
debugID?: string,
}>;
const NativeAnimatedAPI = NativeAnimatedHelper.API;
@@ -97,8 +98,13 @@ export default class AnimatedValue extends AnimatedWithChildren {
this._startingValue = this._value = value;
this._offset = 0;
this._animation = null;
if (config && config.useNativeDriver) {
this.__makeNative();
if (config) {
if (config.useNativeDriver) {
this.__makeNative();
}
if (__DEV__) {
this.__debugID = config.debugID;
}
}
}
@@ -298,6 +304,7 @@ export default class AnimatedValue extends AnimatedWithChildren {
type: 'value',
value: this._value,
offset: this._offset,
debugID: __DEV__ ? this.__debugID : undefined,
};
}
}
@@ -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.
*
* @flow strict-local
* @format
* @oncall react_native
* @fantom_flags enableFixForViewCommandRace:true
*/
import '../../../Core/InitializeCore.js';
import * as ReactNativeTester from '../../../../src/private/__tests__/ReactNativeTester';
import TextInput from '../TextInput';
import * as React from 'react';
import {useEffect, useLayoutEffect, useRef} from 'react';
describe('TextInput', () => {
it('creates view before dispatching view command from ref function', () => {
const root = ReactNativeTester.createRoot();
ReactNativeTester.runTask(() => {
root.render(
<TextInput
ref={node => {
if (node) {
node.focus();
}
}}
/>,
);
});
const mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(2);
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
expect(mountingLogs[1]).toBe(
'dispatch command `focus` on component `AndroidTextInput`',
);
});
it('creates view before dispatching view command from useLayoutEffect', () => {
const root = ReactNativeTester.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
useLayoutEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} />;
}
ReactNativeTester.runTask(() => {
root.render(<Component />);
});
const mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(2);
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
expect(mountingLogs[1]).toBe(
'dispatch command `focus` on component `AndroidTextInput`',
);
});
it('creates view before dispatching view command from useEffect', () => {
const root = ReactNativeTester.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
useEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} />;
}
ReactNativeTester.runTask(() => {
root.render(<Component />);
});
const mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(2);
expect(mountingLogs[0]).toBe('create view type: `AndroidTextInput`');
expect(mountingLogs[1]).toBe(
'dispatch command `focus` on component `AndroidTextInput`',
);
});
});
@@ -7,6 +7,7 @@
* @flow strict-local
* @format
* @oncall react_native
* @fantom_flags enableAccessToHostTreeInFabric:false
*/
import setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests';
@@ -7,9 +7,9 @@
* @flow strict-local
* @format
* @oncall react_native
* @fantom_flags enableAccessToHostTreeInFabric:true
*/
import './setUpFeatureFlags';
import setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests';
setUpReactFabricPublicInstanceFantomTests({isModern: true});
@@ -1,16 +0,0 @@
/**
* 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
* @oncall react_native
*/
import * as ReactNativeFeatureFlags from '../../../../src/private/featureflags/ReactNativeFeatureFlags';
ReactNativeFeatureFlags.override({
enableAccessToHostTreeInFabric: () => true,
});
@@ -0,0 +1,245 @@
/**
* 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
* @oncall react_native
*/
import '../../Core/InitializeCore.js';
import * as ReactNativeTester from '../../../src/private/__tests__/ReactNativeTester';
import View from '../../Components/View/View';
import * as React from 'react';
import {Suspense, startTransition} from 'react';
let resolveFunction: (() => void) | null = null;
// This is a workaround for a bug to get the demo running.
// TODO: replace with real implementation when the bug is fixed.
// $FlowFixMe: [missing-local-annot]
function use(promise) {
if (promise.status === 'fulfilled') {
return promise.value;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise;
} else {
promise.status = 'pending';
promise.then(
result => {
promise.status = 'fulfilled';
promise.value = result;
},
reason => {
promise.status = 'rejected';
promise.reason = reason;
},
);
throw promise;
}
}
type SquareData = {
color: 'red' | 'green',
};
enum SquareId {
Green = 'green-square',
Red = 'red-square',
}
async function getGreenSquareData(): Promise<SquareData> {
await new Promise(resolve => {
resolveFunction = resolve;
});
return {
color: 'green',
};
}
async function getRedSquareData(): Promise<SquareData> {
await new Promise(resolve => {
resolveFunction = resolve;
});
return {
color: 'red',
};
}
const cache = new Map<SquareId, SquareData>();
async function getData(squareId: SquareId): Promise<SquareData> {
switch (squareId) {
case SquareId.Green:
return await getGreenSquareData();
case SquareId.Red:
return await getRedSquareData();
}
}
async function fetchData(squareId: SquareId): Promise<SquareData> {
const data = await getData(squareId);
cache.set(squareId, data);
return data;
}
function Square(props: {squareId: SquareId}) {
let data = cache.get(props.squareId);
if (data == null) {
data = use(fetchData(props.squareId));
}
return <View key={data.color} nativeID={'square with data: ' + data.color} />;
}
function GreenSquare() {
return <Square squareId={SquareId.Green} />;
}
function RedSquare() {
return <Square squareId={SquareId.Red} />;
}
function Fallback() {
return <View nativeID="suspense fallback" />;
}
describe('Suspense', () => {
it('shows fallback if data is not available', () => {
cache.clear();
const root = ReactNativeTester.createRoot();
ReactNativeTester.runTask(() => {
root.render(
<Suspense fallback={<Fallback />}>
<GreenSquare />
</Suspense>,
);
});
let mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `suspense fallback`',
);
expect(resolveFunction).not.toBeNull();
ReactNativeTester.runTask(() => {
resolveFunction?.();
resolveFunction = null;
});
mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `square with data: green`',
);
ReactNativeTester.runTask(() => {
root.render(
<Suspense fallback={<Fallback />}>
<RedSquare />
</Suspense>,
);
});
mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `suspense fallback`',
);
expect(resolveFunction).not.toBeNull();
ReactNativeTester.runTask(() => {
resolveFunction?.();
resolveFunction = null;
});
mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `square with data: red`',
);
ReactNativeTester.runTask(() => {
root.render(
<Suspense fallback={<Fallback />}>
<GreenSquare />
</Suspense>,
);
});
mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `square with data: green`',
);
expect(resolveFunction).toBeNull();
root.destroy();
});
// TODO(T207868872): this test only succeeds with enableFabricCompleteRootInCommitPhase enabled.
// enableFabricCompleteRootInCommitPhase is hardcoded to true in the testing environment.
it('shows stale data while transition is happening', () => {
cache.clear();
cache.set(SquareId.Green, {color: 'green'});
const root = ReactNativeTester.createRoot();
function App(props: {color: 'red' | 'green'}) {
return (
<Suspense fallback={<Fallback />}>
{props.color === 'green' ? <GreenSquare /> : <RedSquare />}
</Suspense>
);
}
ReactNativeTester.runTask(() => {
root.render(<App color="green" />);
});
let mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `square with data: green`',
);
expect(resolveFunction).toBeNull();
ReactNativeTester.runTask(() => {
startTransition(() => {
root.render(<App color="red" />);
});
});
mountingLogs = root.getMountingLogs();
// Green square is still mounted. Fallback is not shown to the user.
expect(mountingLogs.length).toBe(0);
expect(resolveFunction).not.toBeNull();
ReactNativeTester.runTask(() => {
resolveFunction?.();
resolveFunction = null;
});
mountingLogs = root.getMountingLogs();
expect(mountingLogs.length).toBe(1);
expect(mountingLogs[0]).toBe(
'create view type: `View` nativeId: `square with data: red`',
);
root.destroy();
});
});
+6 -1
View File
@@ -12,7 +12,7 @@ import {Constructor} from '../../types/private/Utilities';
import {AccessibilityProps} from '../Components/View/ViewAccessibility';
import {NativeMethods} from '../../types/public/ReactNativeTypes';
import {ColorValue, StyleProp} from '../StyleSheet/StyleSheet';
import {TextStyle} from '../StyleSheet/StyleSheetTypes';
import {TextStyle, ViewStyle} from '../StyleSheet/StyleSheetTypes';
import {
GestureResponderEvent,
LayoutChangeEvent,
@@ -209,6 +209,11 @@ export interface TextProps
* Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
*/
minimumFontScale?: number | undefined;
/**
* Controls how touch events are handled. Similar to `View`'s `pointerEvents`.
*/
pointerEvents?: ViewStyle['pointerEvents'] | undefined;
}
/**
@@ -424,6 +424,7 @@ export type AnimationConfig = $ReadOnly<{
onComplete?: ?EndCallback,
iterations?: number,
isLooping?: boolean,
debugID?: ?string,
...
}>;
declare export default class Animation {
@@ -431,6 +432,7 @@ declare export default class Animation {
__isInteraction: boolean;
__isLooping: ?boolean;
__iterations: number;
__debugID: ?string;
constructor(config: AnimationConfig): void;
start(
fromValue: number,
@@ -959,6 +961,7 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
__getPlatformConfig(): ?PlatformConfig;
__setPlatformConfig(platformConfig: ?PlatformConfig): void;
toJSON(): mixed;
__debugID: ?string;
}
"
`;
@@ -1107,6 +1110,7 @@ declare export default class AnimatedTransform extends AnimatedWithChildren {
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedValue.js 1`] = `
"export type AnimatedValueConfig = $ReadOnly<{
useNativeDriver: boolean,
debugID?: string,
}>;
declare export function flushValue(rootNode: AnimatedNode): void;
declare export default class AnimatedValue extends AnimatedWithChildren {
@@ -468,7 +468,8 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init);
- (dispatch_queue_t)methodQueue
{
if (_bridge.valid) {
RCTAssert(_methodQueue != nullptr, @"Module %@ has no methodQueue (instance: %@)", self, self.instance);
id instance = self.instance;
RCTAssert(_methodQueue != nullptr, @"Module %@ has no methodQueue (instance: %@)", self, instance);
}
return _methodQueue;
}
@@ -13,20 +13,18 @@ namespace facebook::react {
void RCTDefaultCxxLogFunction(ReactNativeLogLevel level, const char *message)
{
NSString *messageString = [NSString stringWithUTF8String:message];
switch (level) {
case ReactNativeLogLevelInfo:
LOG(INFO) << message;
RCTLogInfo(@"%@", messageString);
RCTLogInfo(@"%@", [NSString stringWithUTF8String:message]);
break;
case ReactNativeLogLevelWarning:
LOG(WARNING) << message;
RCTLogWarn(@"%@", messageString);
RCTLogWarn(@"%@", [NSString stringWithUTF8String:message]);
break;
case ReactNativeLogLevelError:
LOG(ERROR) << message;
RCTLogError(@"%@", messageString);
RCTLogError(@"%@", [NSString stringWithUTF8String:message]);
break;
case ReactNativeLogLevelFatal:
LOG(FATAL) << message;
@@ -189,10 +189,11 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
UIEdgeInsets newEdgeInsets = _scrollView.contentInset;
CGFloat inset = MAX(scrollViewLowerY - keyboardEndFrame.origin.y, 0);
const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (isInverted) {
newEdgeInsets.top = MAX(inset, _scrollView.contentInset.top);
newEdgeInsets.top = MAX(inset, props.contentInset.top);
} else {
newEdgeInsets.bottom = MAX(inset, _scrollView.contentInset.bottom);
newEdgeInsets.bottom = MAX(inset, props.contentInset.bottom);
}
CGPoint newContentOffset = _scrollView.contentOffset;
@@ -210,12 +211,6 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
contentDiff = keyboardEndFrame.origin.y - keyboardBeginFrame.origin.y;
}
} else {
CGRect viewIntersection = CGRectIntersection(self.firstResponderFocus, keyboardEndFrame);
if (CGRectIsNull(viewIntersection)) {
return;
}
// Inner text field focused
CGFloat focusEnd = CGRectGetMaxY(self.firstResponderFocus);
if (focusEnd > keyboardEndFrame.origin.y) {
@@ -247,7 +242,7 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
animations:^{
self->_scrollView.contentInset = newEdgeInsets;
self->_scrollView.verticalScrollIndicatorInsets = newEdgeInsets;
[self scrollToOffset:newContentOffset animated:NO];
[self scrollTo:newContentOffset.x y:newContentOffset.y animated:NO];
}
completion:nil];
}
@@ -73,9 +73,8 @@ static void RCTPerformMountInstructions(
case ShadowViewMutation::Insert: {
auto &newChildShadowView = mutation.newChildShadowView;
auto &parentShadowView = mutation.parentShadowView;
auto &newChildViewDescriptor = [registry componentViewDescriptorWithTag:newChildShadowView.tag];
auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:parentShadowView.tag];
auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:mutation.parentTag];
UIView<RCTComponentViewProtocol> *newChildComponentView = newChildViewDescriptor.view;
@@ -94,9 +93,8 @@ static void RCTPerformMountInstructions(
case ShadowViewMutation::Remove: {
auto &oldChildShadowView = mutation.oldChildShadowView;
auto &parentShadowView = mutation.parentShadowView;
auto &oldChildViewDescriptor = [registry componentViewDescriptorWithTag:oldChildShadowView.tag];
auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:parentShadowView.tag];
auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:mutation.parentTag];
[parentViewDescriptor.view unmountChildComponentView:oldChildViewDescriptor.view index:mutation.index];
break;
}
@@ -322,9 +322,8 @@ static NSDictionary *deviceOrientationEventBody(UIDeviceOrientation orientation)
NSNumber *reactTag = rootView.reactTag;
RCTAssert(RCTIsReactRootView(reactTag), @"View %@ with tag #%@ is not a root view", rootView, reactTag);
UIView *existingView = _viewRegistry[reactTag];
RCTAssert(
existingView == nil || existingView == rootView,
_viewRegistry[reactTag] == nil || _viewRegistry[reactTag] == rootView,
@"Expect all root views to have unique tag. Added %@ twice",
reactTag);
@@ -74,11 +74,7 @@ static SEL selectorForType(NSString *type)
if (!_manager && [self isBridgeMode]) {
_manager = [_bridge moduleForClass:_managerClass];
} else if (!_manager && !_bridgelessViewManager) {
_bridgelessViewManager = [_managerClass new];
_bridgelessViewManager.bridge = _bridge;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDidInitializeModuleNotification
object:nil
userInfo:@{@"module" : _bridgelessViewManager}];
_bridgelessViewManager = [_bridge moduleForClass:_managerClass];
}
return _manager ? _manager : _bridgelessViewManager;
}
@@ -2612,6 +2612,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
public fun dispatchCommand (IILcom/facebook/react/bridge/ReadableArray;)V
public fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public fun dispatchCommand (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public fun experimental_prefetchResource (Ljava/lang/String;IILcom/facebook/react/common/mapbuffer/ReadableMapBuffer;)V
public fun getColor (I[Ljava/lang/String;)I
public fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher;
public fun getInspectorDataForInstance (ILandroid/view/View;)Lcom/facebook/react/bridge/ReadableMap;
@@ -2955,6 +2956,7 @@ public abstract interface annotation class com/facebook/react/module/annotations
public final class com/facebook/react/module/model/ReactModuleInfo {
public static final field Companion Lcom/facebook/react/module/model/ReactModuleInfo$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZ)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZZ)V
public final fun canOverrideExistingModule ()Z
public static final fun classIsTurboModule (Ljava/lang/Class;)Z
public final fun className ()Ljava/lang/String;
@@ -3422,11 +3424,12 @@ public abstract interface class com/facebook/react/modules/network/CustomClientB
public abstract fun apply (Lokhttp3/OkHttpClient$Builder;)V
}
public class com/facebook/react/modules/network/ForwardingCookieHandler : java/net/CookieHandler {
public final class com/facebook/react/modules/network/ForwardingCookieHandler : java/net/CookieHandler {
public fun <init> ()V
public fun <init> (Lcom/facebook/react/bridge/ReactContext;)V
public fun addCookies (Ljava/lang/String;Ljava/util/List;)V
public fun clearCookies (Lcom/facebook/react/bridge/Callback;)V
public fun destroy ()V
public final fun addCookies (Ljava/lang/String;Ljava/util/List;)V
public final fun clearCookies (Lcom/facebook/react/bridge/Callback;)V
public final fun destroy ()V
public fun get (Ljava/net/URI;Ljava/util/Map;)Ljava/util/Map;
public fun put (Ljava/net/URI;Ljava/util/Map;)V
}
@@ -3508,14 +3511,6 @@ public class com/facebook/react/modules/network/ProgressResponseBody : okhttp3/R
public fun totalBytesRead ()J
}
public class com/facebook/react/modules/network/ReactCookieJarContainer : com/facebook/react/modules/network/CookieJarContainer {
public fun <init> ()V
public fun loadForRequest (Lokhttp3/HttpUrl;)Ljava/util/List;
public fun removeCookieJar ()V
public fun saveFromResponse (Lokhttp3/HttpUrl;Ljava/util/List;)V
public fun setCookieJar (Lokhttp3/CookieJar;)V
}
public class com/facebook/react/modules/network/ResponseUtil {
public fun <init> ()V
public static fun onDataReceived (Lcom/facebook/react/bridge/ReactApplicationContext;ILcom/facebook/react/bridge/WritableMap;)V
@@ -7770,6 +7765,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro
protected fun dispatchSetPressed (Z)V
public fun draw (Landroid/graphics/Canvas;)V
protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z
public fun endViewTransition (Landroid/view/View;)V
protected fun getChildDrawingOrder (II)I
public fun getClippingRect (Landroid/graphics/Rect;)V
public fun getHitSlopRect ()Landroid/graphics/Rect;
@@ -878,6 +878,16 @@ public class FabricUIManager
}
}
/**
* This method initiates preloading of an image specified by ImageSource. It can later be consumed
* by an ImageView.
*/
public void experimental_prefetchResource(
String componentName, int surfaceId, int reactTag, ReadableMapBuffer params) {
mMountingManager.experimental_prefetchResource(
mReactApplicationContext, componentName, surfaceId, reactTag, params);
}
public void setBinding(FabricUIManagerBinding binding) {
mBinding = binding;
}
@@ -960,7 +970,6 @@ public class FabricUIManager
* @param reactTag
* @param eventName
* @param canCoalesceEvent
* @param customCoalesceKey
* @param params
* @param eventCategory
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<6d8d8f4b81d7be882b315d0960499dcb>>
* @generated SignedSource<<4a219bb47b1b9d988a164bca19eb4fa9>>
*/
/**
@@ -202,6 +202,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun excludeYogaFromRawProps(): Boolean = accessor.excludeYogaFromRawProps()
/**
* Fixes a bug in Differentiator where parent views may be referenced before they're created
*/
@JvmStatic
public fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean = accessor.fixDifferentiatorEmittingUpdatesWithWrongParentTag()
/**
* Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React.
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<031fce8e8b4c20a3e3d6dbecf94d138a>>
* @generated SignedSource<<d75efd6beee8dd9d38b5d648fbecbcda>>
*/
/**
@@ -49,6 +49,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
private var enableUIConsistencyCache: Boolean? = null
private var enableViewRecyclingCache: Boolean? = null
private var excludeYogaFromRawPropsCache: Boolean? = null
private var fixDifferentiatorEmittingUpdatesWithWrongParentTagCache: Boolean? = null
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
private var fixMountingCoordinatorReportedPendingTransactionsOnAndroidCache: Boolean? = null
private var fuseboxEnabledDebugCache: Boolean? = null
@@ -328,6 +329,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
return cached
}
override fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean {
var cached = fixDifferentiatorEmittingUpdatesWithWrongParentTagCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.fixDifferentiatorEmittingUpdatesWithWrongParentTag()
fixDifferentiatorEmittingUpdatesWithWrongParentTagCache = cached
}
return cached
}
override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean {
var cached = fixMappingOfEventPrioritiesBetweenFabricAndReactCache
if (cached == null) {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<35811667ac2543e1f64e27bbdb483ec1>>
* @generated SignedSource<<7454ab19a01cfbb0a54f14bd83fc3a90>>
*/
/**
@@ -86,6 +86,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun excludeYogaFromRawProps(): Boolean
@DoNotStrip @JvmStatic public external fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean
@DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean
@DoNotStrip @JvmStatic public external fun fixMountingCoordinatorReportedPendingTransactionsOnAndroid(): Boolean
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9d829c58e49164a0b2b6b66bc0ce088a>>
* @generated SignedSource<<70d951b2956759280afae4af8f9a2869>>
*/
/**
@@ -81,6 +81,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun excludeYogaFromRawProps(): Boolean = false
override fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean = true
override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false
override fun fixMountingCoordinatorReportedPendingTransactionsOnAndroid(): Boolean = false
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<0121e113410a5b0e14eaf74a3076df2f>>
* @generated SignedSource<<f60000cb58a9632c3aa193854be3de4e>>
*/
/**
@@ -53,6 +53,7 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
private var enableUIConsistencyCache: Boolean? = null
private var enableViewRecyclingCache: Boolean? = null
private var excludeYogaFromRawPropsCache: Boolean? = null
private var fixDifferentiatorEmittingUpdatesWithWrongParentTagCache: Boolean? = null
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
private var fixMountingCoordinatorReportedPendingTransactionsOnAndroidCache: Boolean? = null
private var fuseboxEnabledDebugCache: Boolean? = null
@@ -361,6 +362,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
return cached
}
override fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean {
var cached = fixDifferentiatorEmittingUpdatesWithWrongParentTagCache
if (cached == null) {
cached = currentProvider.fixDifferentiatorEmittingUpdatesWithWrongParentTag()
accessedFeatureFlags.add("fixDifferentiatorEmittingUpdatesWithWrongParentTag")
fixDifferentiatorEmittingUpdatesWithWrongParentTagCache = cached
}
return cached
}
override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean {
var cached = fixMappingOfEventPrioritiesBetweenFabricAndReactCache
if (cached == null) {
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2787d9027695dd14ec6b917a32a1a6de>>
* @generated SignedSource<<d62af893c5d18a2152f098ff305ae41e>>
*/
/**
@@ -81,6 +81,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun excludeYogaFromRawProps(): Boolean
@DoNotStrip public fun fixDifferentiatorEmittingUpdatesWithWrongParentTag(): Boolean
@DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean
@DoNotStrip public fun fixMountingCoordinatorReportedPendingTransactionsOnAndroid(): Boolean
@@ -21,6 +21,24 @@ public class ReactModuleInfo(
public val isCxxModule: Boolean,
public val isTurboModule: Boolean
) {
@Deprecated(
"This constructor is deprecated and will be removed in the future. Use ReactModuleInfo(String, String, boolean, boolean, boolean, boolean)]",
replaceWith =
ReplaceWith(
expression =
"ReactModuleInfo(name, className, canOverrideExistingModule, needsEagerInit, isCxxModule, isTurboModule)"),
level = DeprecationLevel.WARNING)
public constructor(
name: String,
className: String,
canOverrideExistingModule: Boolean,
needsEagerInit: Boolean,
@Suppress("UNUSED_PARAMETER") hasConstants: Boolean,
isCxxModule: Boolean,
isTurboModule: Boolean
) : this(name, className, canOverrideExistingModule, needsEagerInit, isCxxModule, isTurboModule)
public companion object {
/**
* Checks if the passed class is a TurboModule. Useful to populate the parameter [isTurboModule]
@@ -167,7 +167,7 @@ constructor(
// make sure to forward cookies for any requests via the okHttpClient
// so that image requests to endpoints that use cookies still work
val container = OkHttpCompat.getCookieJarContainer(client)
val handler = ForwardingCookieHandler(context)
val handler = ForwardingCookieHandler()
container.setCookieJar(JavaNetCookieJar(handler))
return newBuilder(context.applicationContext, client)
.setNetworkFetcher(ReactOkHttpNetworkFetcher(client))
@@ -1,17 +0,0 @@
/*
* 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.
*/
package com.facebook.react.modules.network;
import okhttp3.CookieJar;
public interface CookieJarContainer extends CookieJar {
void setCookieJar(CookieJar cookieJar);
void removeCookieJar();
}
@@ -0,0 +1,16 @@
/*
* 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.
*/
package com.facebook.react.modules.network
import okhttp3.CookieJar
public interface CookieJarContainer : CookieJar {
public fun setCookieJar(cookieJar: CookieJar)
public fun removeCookieJar()
}
@@ -1,124 +0,0 @@
/*
* 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.
*/
package com.facebook.react.modules.network;
import android.text.TextUtils;
import android.webkit.CookieManager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Cookie handler that forwards all cookies to the WebView CookieManager.
*
* <p>This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
* application is terminated before it syncs.
*/
public class ForwardingCookieHandler extends CookieHandler {
private static final String VERSION_ZERO_HEADER = "Set-cookie";
private static final String VERSION_ONE_HEADER = "Set-cookie2";
private static final String COOKIE_HEADER = "Cookie";
private final ReactContext mContext;
private @Nullable CookieManager mCookieManager;
public ForwardingCookieHandler(ReactContext context) {
mContext = context;
}
@Override
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers)
throws IOException {
CookieManager cookieManager = getCookieManager();
if (cookieManager == null) return Collections.emptyMap();
String cookies = cookieManager.getCookie(uri.toString());
if (TextUtils.isEmpty(cookies)) {
return Collections.emptyMap();
}
return Collections.singletonMap(COOKIE_HEADER, Collections.singletonList(cookies));
}
@Override
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
String url = uri.toString();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String key = entry.getKey();
if (key != null && isCookieHeader(key)) {
addCookies(url, entry.getValue());
}
}
}
public void clearCookies(final Callback callback) {
CookieManager cookieManager = getCookieManager();
if (cookieManager != null) {
cookieManager.removeAllCookies(value -> callback.invoke(value));
}
}
public void destroy() {}
public void addCookies(final String url, final List<String> cookies) {
final CookieManager cookieManager = getCookieManager();
if (cookieManager == null) return;
for (String cookie : cookies) {
addCookieAsync(url, cookie);
}
cookieManager.flush();
}
private void addCookieAsync(String url, String cookie) {
CookieManager cookieManager = getCookieManager();
if (cookieManager != null) {
cookieManager.setCookie(url, cookie, null);
}
}
private static boolean isCookieHeader(String name) {
return name.equalsIgnoreCase(VERSION_ZERO_HEADER) || name.equalsIgnoreCase(VERSION_ONE_HEADER);
}
/**
* Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily
* to make sure it's done on a background thread as needed.
*/
private @Nullable CookieManager getCookieManager() {
if (mCookieManager == null) {
try {
mCookieManager = CookieManager.getInstance();
} catch (IllegalArgumentException ex) {
// https://bugs.chromium.org/p/chromium/issues/detail?id=559720
return null;
} catch (Exception exception) {
// Ideally we would like to catch a `MissingWebViewPackageException` here.
// That API is private so we can't access it.
// Historically we used string matching on the error message to understand
// if the exception was a Missing Webview One.
// OEMs have been customizing that message making really hard to catch it.
// Therefore we result to returning null as a default instead of rethrowing
// the exception as it will result in a app crash at runtime.
// a) We will return null for all the other unhandled conditions when a webview provider is
// not found.
// b) We already have null checks in place for `getCookieManager()` calls.
// c) We have annotated the method as @Nullable to notify future devs about our return type.
return null;
}
}
return mCookieManager;
}
}
@@ -0,0 +1,107 @@
/*
* 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.
*/
package com.facebook.react.modules.network
import android.webkit.CookieManager
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.ReactContext
import java.io.IOException
import java.net.CookieHandler
import java.net.URI
/**
* Cookie handler that forwards all cookies to the WebView CookieManager.
*
* This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
* application is terminated before it syncs.
*/
public class ForwardingCookieHandler() : CookieHandler() {
@Deprecated("Use the default constructor", ReplaceWith("ForwardingCookieHandler()"))
public constructor(@Suppress("UNUSED_PARAMETER") reactContext: ReactContext) : this() {}
@Throws(IOException::class)
override fun get(uri: URI, headers: Map<String, List<String>>): Map<String, List<String>> {
val cookies = cookieManager?.getCookie(uri.toString())
if (cookies.isNullOrEmpty()) {
return emptyMap()
}
return mapOf(COOKIE_HEADER to listOf(cookies))
}
@Throws(IOException::class)
override fun put(uri: URI, headers: Map<String, List<String>>) {
val url = uri.toString()
for ((key, value) in headers) {
if (isCookieHeader(key)) {
addCookies(url, value)
}
}
}
public fun clearCookies(callback: Callback): Unit {
cookieManager?.removeAllCookies { value -> callback.invoke(value) }
}
public fun destroy(): Unit = Unit
public fun addCookies(url: String, cookies: List<String>): Unit {
for (cookie in cookies) {
addCookieAsync(url, cookie)
}
cookieManager?.flush()
}
private fun addCookieAsync(url: String, cookie: String) {
cookieManager?.setCookie(url, cookie, null)
}
private var cookieManager: CookieManager? = null
/**
* Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily
* to make sure it's done on a background thread as needed.
*/
get() {
if (field == null) {
try {
field = CookieManager.getInstance()
} catch (ex: IllegalArgumentException) {
// https://bugs.chromium.org/p/chromium/issues/detail?id=559720
return null
} catch (exception: Exception) {
// Ideally we would like to catch a `MissingWebViewPackageException` here.
// That API is private so we can't access it.
// Historically we used string matching on the error message to understand
// if the exception was a Missing Webview One.
// OEMs have been customizing that message making really hard to catch it.
// Therefore we result to returning null as a default instead of rethrowing
// the exception as it will result in a app crash at runtime.
// a) We will return null for all the other unhandled conditions when a webview provider
// is
// not found.
// b) We already have null checks in place for `getCookieManager()` calls.
// c) We have annotated the method as @Nullable to notify future devs about our return
// type.
return null
}
}
return field
}
private companion object {
private const val VERSION_ZERO_HEADER = "Set-cookie"
private const val VERSION_ONE_HEADER = "Set-cookie2"
private const val COOKIE_HEADER = "Cookie"
private fun isCookieHeader(name: String): Boolean =
name.equals(VERSION_ZERO_HEADER, ignoreCase = true) ||
name.equals(VERSION_ONE_HEADER, ignoreCase = true)
}
}
@@ -97,14 +97,14 @@ public final class NetworkingModule extends NativeNetworkingAndroidSpec {
customClientBuilder = null;
private final OkHttpClient mClient;
private final ForwardingCookieHandler mCookieHandler;
private final ForwardingCookieHandler mCookieHandler = new ForwardingCookieHandler();
private final @Nullable String mDefaultUserAgent;
private final CookieJarContainer mCookieJarContainer;
private final Set<Integer> mRequestIds;
private final Set<Integer> mRequestIds = new HashSet<>();
private final List<RequestBodyHandler> mRequestBodyHandlers = new ArrayList<>();
private final List<UriHandler> mUriHandlers = new ArrayList<>();
private final List<ResponseHandler> mResponseHandlers = new ArrayList<>();
private boolean mShuttingDown;
private boolean mShuttingDown = false;
public NetworkingModule(
ReactApplicationContext reactContext,
@@ -121,11 +121,8 @@ public final class NetworkingModule extends NativeNetworkingAndroidSpec {
client = clientBuilder.build();
}
mClient = client;
mCookieHandler = new ForwardingCookieHandler(reactContext);
mCookieJarContainer = (CookieJarContainer) mClient.cookieJar();
mShuttingDown = false;
mCookieJarContainer = (CookieJarContainer) client.cookieJar();
mDefaultUserAgent = defaultUserAgent;
mRequestIds = new HashSet<>();
}
/**
@@ -1,58 +0,0 @@
/*
* 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.
*/
package com.facebook.react.modules.network;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Headers;
import okhttp3.HttpUrl;
/** Basic okhttp3 CookieJar container */
public class ReactCookieJarContainer implements CookieJarContainer {
@Nullable private CookieJar cookieJar = null;
@Override
public void setCookieJar(CookieJar cookieJar) {
this.cookieJar = cookieJar;
}
@Override
public void removeCookieJar() {
cookieJar = null;
}
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookieJar != null) {
cookieJar.saveFromResponse(url, cookies);
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
if (cookieJar != null) {
List<Cookie> cookies = cookieJar.loadForRequest(url);
ArrayList<Cookie> validatedCookies = new ArrayList<>();
for (Cookie cookie : cookies) {
try {
Headers.Builder cookieChecker = new Headers.Builder();
cookieChecker.add(cookie.name(), cookie.value());
validatedCookies.add(cookie);
} catch (IllegalArgumentException ignored) {
}
}
return validatedCookies;
}
return Collections.emptyList();
}
}
@@ -0,0 +1,48 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions
package com.facebook.react.modules.network
import java.util.ArrayList
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.HttpUrl
/** Basic okhttp3 CookieJar container */
internal class ReactCookieJarContainer : CookieJarContainer {
private var cookieJar: CookieJar? = null
override fun setCookieJar(cookieJar: CookieJar) {
this.cookieJar = cookieJar
}
override fun removeCookieJar() {
cookieJar = null
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookieJar?.saveFromResponse(url, cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookieJar = cookieJar ?: return emptyList()
val cookies = cookieJar.loadForRequest(url)
val validatedCookies = ArrayList<Cookie>()
for (cookie in cookies) {
try {
val cookieChecker = Headers.Builder()
cookieChecker.add(cookie.name(), cookie.value())
validatedCookies.add(cookie)
} catch (ignored: IllegalArgumentException) {}
}
return validatedCookies
}
}
@@ -46,14 +46,12 @@ public final class WebSocketModule extends NativeWebSocketModuleSpec {
private final Map<Integer, WebSocket> mWebSocketConnections = new ConcurrentHashMap<>();
private final Map<Integer, ContentHandler> mContentHandlers = new ConcurrentHashMap<>();
private ForwardingCookieHandler mCookieHandler;
private final ForwardingCookieHandler mCookieHandler = new ForwardingCookieHandler();
private static @Nullable CustomClientBuilder customClientBuilder = null;
public WebSocketModule(ReactApplicationContext context) {
super(context);
mCookieHandler = new ForwardingCookieHandler(context);
}
public static void setCustomClientBuilder(CustomClientBuilder ccb) {
@@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.uimanager;
package com.facebook.react.uimanager
import android.view.MotionEvent;
import android.view.View;
import android.view.MotionEvent
import android.view.View
/** Interface for the root native view of a React native application. */
public interface RootView {
@@ -17,21 +17,20 @@ public interface RootView {
* Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called
* from the child's onTouchIntercepted implementation.
*/
void onChildStartedNativeGesture(View childView, MotionEvent ev);
public fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent)
/**
* @deprecated
*/
@Deprecated
default void onChildStartedNativeGesture(MotionEvent ev) {
onChildStartedNativeGesture(null, ev);
@Deprecated(
message = "Use onChildStartedNativeGesture with a childView parameter.",
replaceWith = ReplaceWith("onChildStartedNativeGesture"))
public fun onChildStartedNativeGesture(ev: MotionEvent) {
onChildStartedNativeGesture(null, ev)
}
/**
* Called when a child ends a native gesture. Should be called from the child's onTouchIntercepted
* implementation.
*/
void onChildEndedNativeGesture(View childView, MotionEvent ev);
public fun onChildEndedNativeGesture(childView: View, ev: MotionEvent)
void handleException(Throwable t);
public fun handleException(t: Throwable)
}
@@ -495,7 +495,7 @@ public class ReactModalHostView(context: ThemedReactContext) :
return super.onHoverEvent(event)
}
override fun onChildStartedNativeGesture(childView: View, ev: MotionEvent) {
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
eventDispatcher?.let { eventDispatcher ->
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher)
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
@@ -62,9 +62,6 @@ public abstract class ReactClippingViewManager<T : ReactViewGroup> : ViewGroupMa
if (removeClippedSubviews) {
val child = getChildAt(parent, index)
if (child != null) {
if (child.parent != null) {
parent.removeView(child)
}
parent.removeViewWithSubviewClippingEnabled(child)
}
} else {
@@ -138,6 +138,7 @@ public class ReactViewGroup extends ViewGroup
private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper;
private float mBackfaceOpacity;
private String mBackfaceVisibility;
private @Nullable Set<Integer> mChildrenRemovedWhileTransitioning;
/**
* Creates a new `ReactViewGroup` instance.
@@ -172,6 +173,7 @@ public class ReactViewGroup extends ViewGroup
mDrawingOrderHelper = null;
mBackfaceOpacity = 1.f;
mBackfaceVisibility = "visible";
mChildrenRemovedWhileTransitioning = null;
}
/* package */ void recycleView() {
@@ -354,6 +356,7 @@ public class ReactViewGroup extends ViewGroup
return;
}
mRemoveClippedSubviews = removeClippedSubviews;
mChildrenRemovedWhileTransitioning = null;
if (removeClippedSubviews) {
mClippingRect = new Rect();
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
@@ -408,6 +411,26 @@ public class ReactViewGroup extends ViewGroup
updateClippingToRect(mClippingRect);
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mChildrenRemovedWhileTransitioning != null) {
mChildrenRemovedWhileTransitioning.remove(view.getId());
}
}
private void trackChildViewTransition(int childId) {
if (mChildrenRemovedWhileTransitioning == null) {
mChildrenRemovedWhileTransitioning = new HashSet<>();
}
mChildrenRemovedWhileTransitioning.add(childId);
}
private boolean isChildRemovedWhileTransitioning(View child) {
return mChildrenRemovedWhileTransitioning != null
&& mChildrenRemovedWhileTransitioning.contains(child.getId());
}
private void updateClippingToRect(Rect clippingRect) {
Assertions.assertNotNull(mAllChildren);
mInSubviewClippingLoop = true;
@@ -573,6 +596,12 @@ public class ReactViewGroup extends ViewGroup
} else {
setChildrenDrawingOrderEnabled(false);
}
// The parent might not be null in case the child is transitioning.
if (child.getParent() != null) {
trackChildViewTransition(child.getId());
}
super.onViewRemoved(child);
}
@@ -745,6 +774,7 @@ public class ReactViewGroup extends ViewGroup
return (boolean) tag;
}
ViewParent parent = view.getParent();
boolean transitioning = isChildRemovedWhileTransitioning(view);
if (index != null) {
ReactSoftExceptionLogger.logSoftException(
"ReactViewGroup.isViewClipped",
@@ -754,10 +784,12 @@ public class ReactViewGroup extends ViewGroup
+ " parentNull="
+ (parent == null)
+ " parentThis="
+ (parent == this)));
+ (parent == this)
+ " transitioning="
+ transitioning));
}
// fallback - parent *should* be null if the view was removed
if (parent == null) {
// fallback - should be transitioning or have no parent if the view was removed
if (parent == null || transitioning) {
return true;
} else {
Assertions.assertCondition(parent == this);
@@ -320,18 +320,14 @@ inline void writeInsertMountItem(
InstructionBuffer& buffer,
const CppMountItem& mountItem) {
buffer.writeIntArray(std::array<int, 3>{
mountItem.newChildShadowView.tag,
mountItem.parentShadowView.tag,
mountItem.index});
mountItem.newChildShadowView.tag, mountItem.parentTag, mountItem.index});
}
inline void writeRemoveMountItem(
InstructionBuffer& buffer,
const CppMountItem& mountItem) {
buffer.writeIntArray(std::array<int, 3>{
mountItem.oldChildShadowView.tag,
mountItem.parentShadowView.tag,
mountItem.index});
mountItem.oldChildShadowView.tag, mountItem.parentTag, mountItem.index});
}
inline void writeUpdatePropsMountItem(
@@ -377,7 +373,7 @@ inline void writeUpdateLayoutMountItem(
buffer.writeIntArray(std::array<int, 8>{
mountItem.newChildShadowView.tag,
mountItem.parentShadowView.tag,
mountItem.parentTag,
x,
y,
w,
@@ -487,7 +483,7 @@ void FabricMountingManager::executeMount(
}
for (const auto& mutation : mutations) {
const auto& parentShadowView = mutation.parentShadowView;
auto parentTag = mutation.parentTag;
const auto& oldChildShadowView = mutation.oldChildShadowView;
const auto& newChildShadowView = mutation.newChildShadowView;
auto& mutationType = mutation.type;
@@ -509,7 +505,7 @@ void FabricMountingManager::executeMount(
case ShadowViewMutation::Remove: {
if (!isVirtual) {
cppCommonMountItems.push_back(CppMountItem::RemoveMountItem(
parentShadowView, oldChildShadowView, index));
parentTag, oldChildShadowView, index));
}
break;
}
@@ -559,7 +555,7 @@ void FabricMountingManager::executeMount(
(maintainMutationOrder ? cppCommonMountItems
: cppUpdateLayoutMountItems)
.push_back(CppMountItem::UpdateLayoutMountItem(
mutation.newChildShadowView, parentShadowView));
mutation.newChildShadowView, parentTag));
}
// OverflowInset: This is the values indicating boundaries including
@@ -588,7 +584,7 @@ void FabricMountingManager::executeMount(
if (!isVirtual) {
// Insert item
cppCommonMountItems.push_back(CppMountItem::InsertMountItem(
parentShadowView, newChildShadowView, index));
parentTag, newChildShadowView, index));
bool shouldCreateView =
!allocatedViewTags.contains(newChildShadowView.tag);
@@ -625,7 +621,7 @@ void FabricMountingManager::executeMount(
(maintainMutationOrder ? cppCommonMountItems
: cppUpdateLayoutMountItems)
.push_back(CppMountItem::UpdateLayoutMountItem(
newChildShadowView, parentShadowView));
newChildShadowView, parentTag));
// OverflowInset: This is the values indicating boundaries including
// children of the current view. The layout of current view may not
@@ -16,16 +16,16 @@ CppMountItem CppMountItem::DeleteMountItem(const ShadowView& shadowView) {
return {CppMountItem::Type::Delete, {}, shadowView, {}, -1};
}
CppMountItem CppMountItem::InsertMountItem(
const ShadowView& parentView,
Tag parentTag,
const ShadowView& shadowView,
int index) {
return {CppMountItem::Type::Insert, parentView, {}, shadowView, index};
return {CppMountItem::Type::Insert, parentTag, {}, shadowView, index};
}
CppMountItem CppMountItem::RemoveMountItem(
const ShadowView& parentView,
Tag parentTag,
const ShadowView& shadowView,
int index) {
return {CppMountItem::Type::Remove, parentView, shadowView, {}, index};
return {CppMountItem::Type::Remove, parentTag, shadowView, {}, index};
}
CppMountItem CppMountItem::UpdatePropsMountItem(
const ShadowView& oldShadowView,
@@ -38,20 +38,20 @@ CppMountItem CppMountItem::UpdateStateMountItem(const ShadowView& shadowView) {
}
CppMountItem CppMountItem::UpdateLayoutMountItem(
const ShadowView& shadowView,
const ShadowView& parentView) {
return {CppMountItem::Type::UpdateLayout, parentView, {}, shadowView, -1};
Tag parentTag) {
return {CppMountItem::Type::UpdateLayout, parentTag, {}, shadowView, -1};
}
CppMountItem CppMountItem::UpdateEventEmitterMountItem(
const ShadowView& shadowView) {
return {CppMountItem::Type::UpdateEventEmitter, {}, {}, shadowView, -1};
return {CppMountItem::Type::UpdateEventEmitter, -1, {}, shadowView, -1};
}
CppMountItem CppMountItem::UpdatePaddingMountItem(
const ShadowView& shadowView) {
return {CppMountItem::Type::UpdatePadding, {}, {}, shadowView, -1};
return {CppMountItem::Type::UpdatePadding, -1, {}, shadowView, -1};
}
CppMountItem CppMountItem::UpdateOverflowInsetMountItem(
const ShadowView& shadowView) {
return {CppMountItem::Type::UpdateOverflowInset, {}, {}, shadowView, -1};
return {CppMountItem::Type::UpdateOverflowInset, -1, {}, shadowView, -1};
}
} // namespace facebook::react
@@ -24,15 +24,11 @@ struct CppMountItem final {
static CppMountItem DeleteMountItem(const ShadowView& shadowView);
static CppMountItem InsertMountItem(
const ShadowView& parentView,
const ShadowView& shadowView,
int index);
static CppMountItem
InsertMountItem(Tag parentTag, const ShadowView& shadowView, int index);
static CppMountItem RemoveMountItem(
const ShadowView& parentView,
const ShadowView& shadowView,
int index);
static CppMountItem
RemoveMountItem(Tag parentTag, const ShadowView& shadowView, int index);
static CppMountItem UpdatePropsMountItem(
const ShadowView& oldShadowView,
@@ -42,7 +38,7 @@ struct CppMountItem final {
static CppMountItem UpdateLayoutMountItem(
const ShadowView& shadowView,
const ShadowView& parentView);
Tag parentTag);
static CppMountItem UpdateEventEmitterMountItem(const ShadowView& shadowView);
@@ -71,7 +67,7 @@ struct CppMountItem final {
#pragma mark - Fields
Type type = {Create};
ShadowView parentShadowView = {};
Tag parentTag = -1;
ShadowView oldChildShadowView = {};
ShadowView newChildShadowView = {};
int index = {};
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<902b269e45fcb4970c6f8a86818e1940>>
* @generated SignedSource<<640630d7a40b53f7d507569aa6409f69>>
*/
/**
@@ -213,6 +213,12 @@ class ReactNativeFeatureFlagsProviderHolder
return method(javaProvider_);
}
bool fixDifferentiatorEmittingUpdatesWithWrongParentTag() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fixDifferentiatorEmittingUpdatesWithWrongParentTag");
return method(javaProvider_);
}
bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fixMappingOfEventPrioritiesBetweenFabricAndReact");
@@ -464,6 +470,11 @@ bool JReactNativeFeatureFlagsCxxInterop::excludeYogaFromRawProps(
return ReactNativeFeatureFlags::excludeYogaFromRawProps();
}
bool JReactNativeFeatureFlagsCxxInterop::fixDifferentiatorEmittingUpdatesWithWrongParentTag(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::fixDifferentiatorEmittingUpdatesWithWrongParentTag();
}
bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact();
@@ -667,6 +678,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"excludeYogaFromRawProps",
JReactNativeFeatureFlagsCxxInterop::excludeYogaFromRawProps),
makeNativeMethod(
"fixDifferentiatorEmittingUpdatesWithWrongParentTag",
JReactNativeFeatureFlagsCxxInterop::fixDifferentiatorEmittingUpdatesWithWrongParentTag),
makeNativeMethod(
"fixMappingOfEventPrioritiesBetweenFabricAndReact",
JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact),
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<17da0d7937c5c0c533293b86c8cdc9be>>
* @generated SignedSource<<4218168e779a2241d0752771c1f51b12>>
*/
/**
@@ -117,6 +117,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool excludeYogaFromRawProps(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool fixDifferentiatorEmittingUpdatesWithWrongParentTag(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool fixMappingOfEventPrioritiesBetweenFabricAndReact(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
@@ -47,6 +47,11 @@ void WritableNativeArray::pushInt(jint value) {
array_.push_back(value);
}
void WritableNativeArray::pushLong(jlong value) {
throwIfConsumed();
array_.push_back(value);
}
void WritableNativeArray::pushString(jstring value) {
if (value == NULL) {
pushNull();
@@ -81,6 +86,7 @@ void WritableNativeArray::registerNatives() {
makeNativeMethod("pushBoolean", WritableNativeArray::pushBoolean),
makeNativeMethod("pushDouble", WritableNativeArray::pushDouble),
makeNativeMethod("pushInt", WritableNativeArray::pushInt),
makeNativeMethod("pushLong", WritableNativeArray::pushLong),
makeNativeMethod("pushString", WritableNativeArray::pushString),
makeNativeMethod("pushNativeArray", WritableNativeArray::pushNativeArray),
makeNativeMethod("pushNativeMap", WritableNativeArray::pushNativeMap),
@@ -36,6 +36,7 @@ struct WritableNativeArray
void pushBoolean(jboolean value);
void pushDouble(jdouble value);
void pushInt(jint value);
void pushLong(jlong value);
void pushString(jstring value);
void pushNativeArray(ReadableNativeArray* otherArray);
void pushNativeMap(ReadableNativeMap* map);
@@ -44,6 +44,11 @@ void WritableNativeMap::putInt(std::string key, int val) {
map_.insert(std::move(key), val);
}
void WritableNativeMap::putLong(std::string key, jlong val) {
throwIfConsumed();
map_.insert(std::move(key), val);
}
void WritableNativeMap::putString(std::string key, alias_ref<jstring> val) {
if (!val) {
putNull(std::move(key));
@@ -90,6 +95,7 @@ void WritableNativeMap::registerNatives() {
makeNativeMethod("putBoolean", WritableNativeMap::putBoolean),
makeNativeMethod("putDouble", WritableNativeMap::putDouble),
makeNativeMethod("putInt", WritableNativeMap::putInt),
makeNativeMethod("putLong", WritableNativeMap::putLong),
makeNativeMethod("putString", WritableNativeMap::putString),
makeNativeMethod("putNativeArray", WritableNativeMap::putNativeArray),
makeNativeMethod("putNativeMap", WritableNativeMap::putNativeMap),
@@ -35,6 +35,7 @@ struct WritableNativeMap
void putBoolean(std::string key, bool val);
void putDouble(std::string key, double val);
void putInt(std::string key, int val);
void putLong(std::string key, jlong val);
void putString(std::string key, jni::alias_ref<jstring> val);
void putNativeArray(std::string key, ReadableNativeArray* val);
void putNativeMap(std::string key, ReadableNativeMap* val);
@@ -18,6 +18,12 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
import org.robolectric.RobolectricTestRunner
/**
* Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is
* returned.
*/
private fun <T> nonNullAny(type: Class<T>): T = any(type)
/** Tests for {@link NetworkingModule}. */
@RunWith(RobolectricTestRunner::class)
class ReactCookieJarContainerTest {
@@ -33,7 +39,7 @@ class ReactCookieJarContainerTest {
fun testEmptyCookies() {
val jarContainer: ReactCookieJarContainer = mock(ReactCookieJarContainer::class.java)
val cookies: List<Cookie> = emptyList()
whenever(jarContainer.loadForRequest(any(HttpUrl::class.java))).thenReturn(cookies)
whenever(jarContainer.loadForRequest(nonNullAny(HttpUrl::class.java))).thenReturn(cookies)
assertThat(jarContainer.loadForRequest(httpUrl).size).isEqualTo(0)
}
@@ -0,0 +1,252 @@
/*
* 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.
*/
package com.facebook.react.modules.network
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import java.net.SocketTimeoutException
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.MockedStatic
import org.mockito.Mockito.*
import org.mockito.Mockito.mockStatic
class ResponseUtilTest {
private lateinit var reactContext: ReactApplicationContext
private lateinit var arguments: MockedStatic<Arguments>
@Before
fun setUp() {
reactContext = mock(ReactApplicationContext::class.java)
arguments = mockStatic(Arguments::class.java)
arguments.`when`<WritableArray>(Arguments::createArray).thenAnswer { JavaOnlyArray() }
arguments.`when`<WritableMap>(Arguments::createMap).thenAnswer { JavaOnlyMap() }
}
@After
fun tearDown() {
arguments.close()
}
@Test
fun testOnDataSend() {
val requestId = 1
val progress = 100L
val total = 1000L
ResponseUtil.onDataSend(reactContext, requestId, progress, total)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didSendNetworkData")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(3)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getInt(1)).isEqualTo(progress.toInt())
assertThat(args.getInt(2)).isEqualTo(total.toInt())
}
@Test
fun testOnIncrementalDataReceived() {
val requestId = 1
val data = "some data"
val progress = 100L
val total = 1000L
ResponseUtil.onIncrementalDataReceived(reactContext, requestId, data, progress, total)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkIncrementalData")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(4)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getString(1)).isEqualTo(data)
assertThat(args.getInt(2)).isEqualTo(progress.toInt())
assertThat(args.getInt(3)).isEqualTo(total.toInt())
}
@Test
fun testOnDataReceivedProgress() {
val requestId = 1
val progress = 500L
val total = 1000L
ResponseUtil.onDataReceivedProgress(reactContext, requestId, progress, total)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkDataProgress")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(3)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getInt(1)).isEqualTo(progress.toInt())
assertThat(args.getInt(2)).isEqualTo(total.toInt())
}
@Test
fun testOnDataReceived() {
val requestId = 1
val data = "response data"
ResponseUtil.onDataReceived(reactContext, requestId, data)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkData")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(2)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getString(1)).isEqualTo(data)
}
@Test
fun testOnDataReceivedMap() {
val requestId = 1
val data: WritableMap = Arguments.createMap().apply { putString("key", "value") }
ResponseUtil.onDataReceived(reactContext, requestId, data)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkData")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(2)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getMap(1)).isEqualTo(data)
}
@Test
fun testOnRequestError() {
val requestId = 1
val error = "An error occurred"
val e: Throwable? = null
ResponseUtil.onRequestError(reactContext, requestId, error, e)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didCompleteNetworkResponse")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(2)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getString(1)).isEqualTo(error)
}
@Test
fun testOnRequestErrorWithException() {
val requestId = 1
val error = "Timeout error"
val e: Throwable = SocketTimeoutException()
ResponseUtil.onRequestError(reactContext, requestId, error, e)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didCompleteNetworkResponse")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(3)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getString(1)).isEqualTo(error)
assertThat(args.getBoolean(2)).isTrue
}
@Test
fun testOnRequestSuccess() {
val requestId = 1
ResponseUtil.onRequestSuccess(reactContext, requestId)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didCompleteNetworkResponse")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(2)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.isNull(1)).isTrue()
}
@Test
fun testOnResponseReceived() {
val requestId = 1
val statusCode = 200
val headers: WritableMap =
Arguments.createMap().apply { putString("Content-Type", "application/json") }
val url = "http://example.com"
ResponseUtil.onResponseReceived(reactContext, requestId, statusCode, headers, url)
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java)
verify(reactContext).emitDeviceEvent(eventNameCaptor.capture(), eventArgumentsCaptor.capture())
assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkResponse")
val args = eventArgumentsCaptor.value
assertThat(args.size()).isEqualTo(4)
assertThat(args.getInt(0)).isEqualTo(requestId)
assertThat(args.getInt(1)).isEqualTo(statusCode)
assertThat(args.getMap(2)).isEqualTo(headers)
assertThat(args.getString(3)).isEqualTo(url)
}
@Test
fun testNullReactContext() {
ResponseUtil.onDataSend(null, 1, 100, 1000)
ResponseUtil.onIncrementalDataReceived(null, 1, "data", 100, 1000)
ResponseUtil.onDataReceivedProgress(null, 1, 100, 1000)
ResponseUtil.onDataReceived(null, 1, "data")
ResponseUtil.onDataReceived(null, 1, Arguments.createMap())
ResponseUtil.onRequestError(null, 1, "error", null)
ResponseUtil.onRequestSuccess(null, 1)
ResponseUtil.onResponseReceived(null, 1, 200, Arguments.createMap(), "http://example.com")
verify(reactContext, never()).emitDeviceEvent(anyString(), any())
}
}
@@ -29,9 +29,12 @@ bool isLooselyNull(const jsi::Value& value) {
return value.isNull() || value.isUndefined();
}
bool isEmptyString(jsi::Runtime& runtime, const jsi::Value& value) {
bool isEqualTo(
jsi::Runtime& runtime,
const jsi::Value& value,
const std::string& str) {
return jsi::Value::strictEquals(
runtime, value, jsi::String::createFromUtf8(runtime, ""));
runtime, value, jsi::String::createFromUtf8(runtime, str));
}
std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -265,7 +268,7 @@ void JsErrorHandler::handleErrorWithCppPipeline(
}
auto nameValue = errorObj.getProperty(runtime, "name");
auto name = (isLooselyNull(nameValue) || isEmptyString(runtime, nameValue))
auto name = (isLooselyNull(nameValue) || isEqualTo(runtime, nameValue, ""))
? std::nullopt
: std::optional(stringifyToCpp(runtime, nameValue));
@@ -383,14 +386,19 @@ void JsErrorHandler::handleErrorWithCppPipeline(
return;
}
if (isFatal) {
if (_hasHandledFatalError) {
return;
}
_hasHandledFatalError = true;
}
auto errorType = errorObj.getProperty(runtime, "type");
auto isWarn = isEqualTo(runtime, errorType, "warn");
_onJsError(runtime, parsedError);
if (isFatal || !isWarn) {
if (isFatal) {
if (_hasHandledFatalError) {
return;
}
_hasHandledFatalError = true;
}
_onJsError(runtime, parsedError);
}
}
void JsErrorHandler::registerErrorListener(
@@ -232,6 +232,22 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation {
return plain_.utf16(sym);
}
void getStringData(
const jsi::String& str,
void* ctx,
void (
*cb)(void* ctx, bool ascii, const void* data, size_t num)) override {
plain_.getStringData(str, ctx, cb);
}
void getPropNameIdData(
const jsi::PropNameID& sym,
void* ctx,
void (
*cb)(void* ctx, bool ascii, const void* data, size_t num)) override {
plain_.getPropNameIdData(sym, ctx, cb);
}
Object createObject() override {
return plain_.createObject();
};
@@ -690,6 +706,24 @@ class WithRuntimeDecorator : public RuntimeDecorator<Plain, Base> {
return RD::utf16(sym);
}
void getStringData(
const jsi::String& str,
void* ctx,
void (
*cb)(void* ctx, bool ascii, const void* data, size_t num)) override {
Around around{with_};
RD::getStringData(str, ctx, cb);
}
void getPropNameIdData(
const jsi::PropNameID& sym,
void* ctx,
void (
*cb)(void* ctx, bool ascii, const void* data, size_t num)) override {
Around around{with_};
RD::getPropNameIdData(sym, ctx, cb);
}
Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override {
Around around{with_};
return RD::createValueFromJsonUtf8(json, length);
@@ -258,6 +258,22 @@ std::u16string Runtime::utf16(const String& str) {
return convertUTF8ToUTF16(utf8Str);
}
void Runtime::getStringData(
const jsi::String& str,
void* ctx,
void (*cb)(void* ctx, bool ascii, const void* data, size_t num)) {
auto utf16Str = utf16(str);
cb(ctx, false, utf16Str.data(), utf16Str.size());
}
void Runtime::getPropNameIdData(
const jsi::PropNameID& sym,
void* ctx,
void (*cb)(void* ctx, bool ascii, const void* data, size_t num)) {
auto utf16Str = utf16(sym);
cb(ctx, false, utf16Str.data(), utf16Str.size());
}
Pointer& Pointer::operator=(Pointer&& other) noexcept {
if (ptr_) {
ptr_->invalidate();
@@ -402,6 +402,34 @@ class JSI_EXPORT Runtime {
virtual std::u16string utf16(const String& str);
virtual std::u16string utf16(const PropNameID& sym);
/// Invokes the provided callback \p cb with the String content in \p str.
/// The callback must take in three arguments: bool ascii, const void* data,
/// and size_t num, respectively. \p ascii indicates whether the \p data
/// passed to the callback should be interpreted as a pointer to a sequence of
/// \p num ASCII characters or UTF16 characters. Depending on the internal
/// representation of the string, the function may invoke the callback
/// multiple times, with a different format on each invocation. The callback
/// must not access runtime functionality, as any operation on the runtime may
/// invalidate the data pointers.
virtual void getStringData(
const jsi::String& str,
void* ctx,
void (*cb)(void* ctx, bool ascii, const void* data, size_t num));
/// Invokes the provided callback \p cb with the PropNameID content in \p sym.
/// The callback must take in three arguments: bool ascii, const void* data,
/// and size_t num, respectively. \p ascii indicates whether the \p data
/// passed to the callback should be interpreted as a pointer to a sequence of
/// \p num ASCII characters or UTF16 characters. Depending on the internal
/// representation of the string, the function may invoke the callback
/// multiple times, with a different format on each invocation. The callback
/// must not access runtime functionality, as any operation on the runtime may
/// invalidate the data pointers.
virtual void getPropNameIdData(
const jsi::PropNameID& sym,
void* ctx,
void (*cb)(void* ctx, bool ascii, const void* data, size_t num));
// These exist so derived classes can access the private parts of
// Value, Symbol, String, and Object, which are all friends of Runtime.
template <typename T>
@@ -509,6 +537,22 @@ class JSI_EXPORT PropNameID : public Pointer {
return runtime.utf16(*this);
}
/// Invokes the user provided callback to process the content in PropNameId.
/// The callback must take in three arguments: bool ascii, const void* data,
/// and size_t num, respectively. \p ascii indicates whether the \p data
/// passed to the callback should be interpreted as a pointer to a sequence of
/// \p num ASCII characters or UTF16 characters. The function may invoke the
/// callback multiple times, with a different format on each invocation. The
/// callback must not access runtime functionality, as any operation on the
/// runtime may invalidate the data pointers.
template <typename CB>
void getPropNameIdData(Runtime& runtime, CB& cb) const {
runtime.getPropNameIdData(
*this, &cb, [](void* ctx, bool ascii, const void* data, size_t num) {
(*((CB*)ctx))(ascii, data, num);
});
}
static bool compare(
Runtime& runtime,
const jsi::PropNameID& a,
@@ -664,6 +708,22 @@ class JSI_EXPORT String : public Pointer {
return runtime.utf16(*this);
}
/// Invokes the user provided callback to process content in String. The
/// callback must take in three arguments: bool ascii, const void* data, and
/// size_t num, respectively. \p ascii indicates whether the \p data passed to
/// the callback should be interpreted as a pointer to a sequence of \p num
/// ASCII characters or UTF16 characters. The function may invoke the callback
/// multiple times, with a different format on each invocation. The callback
/// must not access runtime functionality, as any operation on the runtime may
/// invalidate the data pointers.
template <typename CB>
void getStringData(Runtime& runtime, CB& cb) const {
runtime.getStringData(
*this, &cb, [](void* ctx, bool ascii, const void* data, size_t num) {
(*((CB*)ctx))(ascii, data, num);
});
}
friend class Runtime;
friend class Value;
};
@@ -1635,6 +1635,35 @@ TEST_P(JSITest, UTF16Test) {
EXPECT_EQ(str.utf16(rd), u"\uFFFD\u007A");
}
TEST_P(JSITest, GetStringDataTest) {
// This Runtime Decorator is used to test the default getStringData
// implementation for VMs that do not provide their own implementation
class RD : public RuntimeDecorator<Runtime, Runtime> {
public:
RD(Runtime& rt) : RuntimeDecorator(rt) {}
void getStringData(
const String& str,
void* ctx,
void (*cb)(void* ctx, bool ascii, const void* data, size_t num))
override {
Runtime::getStringData(str, ctx, cb);
}
};
RD rd = RD(rt);
String str = String::createFromUtf8(rd, "hello👋");
std::u16string buf;
auto cb = [&buf](bool ascii, const void* data, size_t num) {
assert(!ascii && "Default implementation is always utf16");
buf.append((const char16_t*)data, num);
};
str.getStringData(rd, cb);
EXPECT_EQ(buf, str.utf16(rd));
}
INSTANTIATE_TEST_CASE_P(
Runtimes,
JSITest,
@@ -20,31 +20,39 @@ folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
use_frameworks = ENV['USE_FRAMEWORKS'] != nil
header_search_paths = [
"\"$(PODS_TARGET_SRCROOT)/..\"",
"\"$(PODS_ROOT)/boost\"",
"\"$(PODS_ROOT)/DoubleConversion\"",
"\"$(PODS_ROOT)/fast_float/include\"",
"\"$(PODS_ROOT)/fmt/include\"",
"\"$(PODS_ROOT)/RCT-Folly\"",
]
header_dir = 'jsinspector-modern'
module_name = "jsinspector_modern"
Pod::Spec.new do |s|
s.name = "React-jsinspector"
s.version = version
s.summary = "-" # TODO
s.summary = "React Native subsystem for modern debugging over the Chrome DevTools Protocol (CDP)"
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "*.{cpp,h,def}"
s.header_dir = 'jsinspector-modern'
s.header_dir = header_dir
s.compiler_flags = folly_compiler_flags
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/..\" \"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fast_float/include\" \"$(PODS_ROOT)/fmt/include\"",
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"DEFINES_MODULE" => "YES"
}.merge!(use_frameworks ? {
}.merge!(ENV['USE_FRAMEWORKS'] ? {
"PUBLIC_HEADERS_FOLDER_PATH" => "#{module_name}.framework/Headers/#{header_dir}"
} : {})
if use_frameworks
if ENV['USE_FRAMEWORKS']
s.module_name = module_name
end
@@ -18,3 +18,6 @@ file(GLOB react_featureflags_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_featureflags OBJECT ${react_featureflags_SRC})
target_include_directories(react_featureflags PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_featureflags
folly_runtime)
@@ -22,6 +22,10 @@ if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\"" # this is needed to allow the feature flags access its own files
end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
Pod::Spec.new do |s|
s.name = "React-featureflags"
s.version = version
@@ -32,11 +36,14 @@ Pod::Spec.new do |s|
s.platforms = min_supported_versions
s.source = source
s.source_files = "*.{cpp,h}"
s.compiler_flags = folly_compiler_flags
s.header_dir = "react/featureflags"
s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"DEFINES_MODULE" => "YES" }
s.dependency "RCT-Folly", folly_version
if ENV['USE_FRAMEWORKS']
s.module_name = "React_featureflags"
s.header_mappings_dir = "../.."
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ef215623465d45c563030d724287b1c9>>
* @generated SignedSource<<2409869111055ff0b32c1f40c10042d7>>
*/
/**
@@ -142,6 +142,10 @@ bool ReactNativeFeatureFlags::excludeYogaFromRawProps() {
return getAccessor().excludeYogaFromRawProps();
}
bool ReactNativeFeatureFlags::fixDifferentiatorEmittingUpdatesWithWrongParentTag() {
return getAccessor().fixDifferentiatorEmittingUpdatesWithWrongParentTag();
}
bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() {
return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact();
}
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<f741660e4cf2528defe0ab1f61858aab>>
* @generated SignedSource<<e628af8109a1d8bb6425515d824852a3>>
*/
/**
@@ -184,6 +184,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool excludeYogaFromRawProps();
/**
* Fixes a bug in Differentiator where parent views may be referenced before they're created
*/
RN_EXPORT static bool fixDifferentiatorEmittingUpdatesWithWrongParentTag();
/**
* Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React.
*/
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4c3956150bbf826c2abf4f8daf569b88>>
* @generated SignedSource<<1e5b87b564e880cfb1423a85692092ba>>
*/
/**
@@ -551,6 +551,24 @@ bool ReactNativeFeatureFlagsAccessor::excludeYogaFromRawProps() {
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::fixDifferentiatorEmittingUpdatesWithWrongParentTag() {
auto flagValue = fixDifferentiatorEmittingUpdatesWithWrongParentTag_.load();
if (!flagValue.has_value()) {
// This block is not exclusive but it is not necessary.
// If multiple threads try to initialize the feature flag, we would only
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(29, "fixDifferentiatorEmittingUpdatesWithWrongParentTag");
flagValue = currentProvider_->fixDifferentiatorEmittingUpdatesWithWrongParentTag();
fixDifferentiatorEmittingUpdatesWithWrongParentTag_ = flagValue;
}
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAndReact() {
auto flagValue = fixMappingOfEventPrioritiesBetweenFabricAndReact_.load();
@@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(29, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
markFlagAsAccessed(30, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact();
fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue;
@@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMountingCoordinatorReportedPendingTrans
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(30, "fixMountingCoordinatorReportedPendingTransactionsOnAndroid");
markFlagAsAccessed(31, "fixMountingCoordinatorReportedPendingTransactionsOnAndroid");
flagValue = currentProvider_->fixMountingCoordinatorReportedPendingTransactionsOnAndroid();
fixMountingCoordinatorReportedPendingTransactionsOnAndroid_ = flagValue;
@@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledDebug() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(31, "fuseboxEnabledDebug");
markFlagAsAccessed(32, "fuseboxEnabledDebug");
flagValue = currentProvider_->fuseboxEnabledDebug();
fuseboxEnabledDebug_ = flagValue;
@@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(32, "fuseboxEnabledRelease");
markFlagAsAccessed(33, "fuseboxEnabledRelease");
flagValue = currentProvider_->fuseboxEnabledRelease();
fuseboxEnabledRelease_ = flagValue;
@@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::initEagerTurboModulesOnNativeModulesQueueA
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(33, "initEagerTurboModulesOnNativeModulesQueueAndroid");
markFlagAsAccessed(34, "initEagerTurboModulesOnNativeModulesQueueAndroid");
flagValue = currentProvider_->initEagerTurboModulesOnNativeModulesQueueAndroid();
initEagerTurboModulesOnNativeModulesQueueAndroid_ = flagValue;
@@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::lazyAnimationCallbacks() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(34, "lazyAnimationCallbacks");
markFlagAsAccessed(35, "lazyAnimationCallbacks");
flagValue = currentProvider_->lazyAnimationCallbacks();
lazyAnimationCallbacks_ = flagValue;
@@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::loadVectorDrawablesOnImages() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(35, "loadVectorDrawablesOnImages");
markFlagAsAccessed(36, "loadVectorDrawablesOnImages");
flagValue = currentProvider_->loadVectorDrawablesOnImages();
loadVectorDrawablesOnImages_ = flagValue;
@@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(36, "traceTurboModulePromiseRejectionsOnAndroid");
markFlagAsAccessed(37, "traceTurboModulePromiseRejectionsOnAndroid");
flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid();
traceTurboModulePromiseRejectionsOnAndroid_ = flagValue;
@@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(37, "useAlwaysAvailableJSErrorHandling");
markFlagAsAccessed(38, "useAlwaysAvailableJSErrorHandling");
flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling();
useAlwaysAvailableJSErrorHandling_ = flagValue;
@@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(38, "useFabricInterop");
markFlagAsAccessed(39, "useFabricInterop");
flagValue = currentProvider_->useFabricInterop();
useFabricInterop_ = flagValue;
@@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::useImmediateExecutorInAndroidBridgeless()
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(39, "useImmediateExecutorInAndroidBridgeless");
markFlagAsAccessed(40, "useImmediateExecutorInAndroidBridgeless");
flagValue = currentProvider_->useImmediateExecutorInAndroidBridgeless();
useImmediateExecutorInAndroidBridgeless_ = flagValue;
@@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(40, "useNativeViewConfigsInBridgelessMode");
markFlagAsAccessed(41, "useNativeViewConfigsInBridgelessMode");
flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode();
useNativeViewConfigsInBridgelessMode_ = flagValue;
@@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimisedViewPreallocationOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(41, "useOptimisedViewPreallocationOnAndroid");
markFlagAsAccessed(42, "useOptimisedViewPreallocationOnAndroid");
flagValue = currentProvider_->useOptimisedViewPreallocationOnAndroid();
useOptimisedViewPreallocationOnAndroid_ = flagValue;
@@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(42, "useOptimizedEventBatchingOnAndroid");
markFlagAsAccessed(43, "useOptimizedEventBatchingOnAndroid");
flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid();
useOptimizedEventBatchingOnAndroid_ = flagValue;
@@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::useRuntimeShadowNodeReferenceUpdate() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(43, "useRuntimeShadowNodeReferenceUpdate");
markFlagAsAccessed(44, "useRuntimeShadowNodeReferenceUpdate");
flagValue = currentProvider_->useRuntimeShadowNodeReferenceUpdate();
useRuntimeShadowNodeReferenceUpdate_ = flagValue;
@@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(44, "useTurboModuleInterop");
markFlagAsAccessed(45, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(45, "useTurboModules");
markFlagAsAccessed(46, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<3d98085a73dfc51541342dbb42ed89ab>>
* @generated SignedSource<<eb44aabe7e352481267aa9a6bf035ff1>>
*/
/**
@@ -61,6 +61,7 @@ class ReactNativeFeatureFlagsAccessor {
bool enableUIConsistency();
bool enableViewRecycling();
bool excludeYogaFromRawProps();
bool fixDifferentiatorEmittingUpdatesWithWrongParentTag();
bool fixMappingOfEventPrioritiesBetweenFabricAndReact();
bool fixMountingCoordinatorReportedPendingTransactionsOnAndroid();
bool fuseboxEnabledDebug();
@@ -89,7 +90,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 46> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 47> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> completeReactInstanceCreationOnBgThreadOnAndroid_;
@@ -120,6 +121,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> enableUIConsistency_;
std::atomic<std::optional<bool>> enableViewRecycling_;
std::atomic<std::optional<bool>> excludeYogaFromRawProps_;
std::atomic<std::optional<bool>> fixDifferentiatorEmittingUpdatesWithWrongParentTag_;
std::atomic<std::optional<bool>> fixMappingOfEventPrioritiesBetweenFabricAndReact_;
std::atomic<std::optional<bool>> fixMountingCoordinatorReportedPendingTransactionsOnAndroid_;
std::atomic<std::optional<bool>> fuseboxEnabledDebug_;
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<aff3c46b8d2db3bde519e1392569d53d>>
* @generated SignedSource<<ee899be30798eb6d386b44bc6bc027ea>>
*/
/**
@@ -143,6 +143,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return false;
}
bool fixDifferentiatorEmittingUpdatesWithWrongParentTag() override {
return true;
}
bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override {
return false;
}
@@ -0,0 +1,472 @@
/*
* 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.
*
* @generated SignedSource<<a3ad884be5b18b3cfad650941abfa751>>
*/
/**
* IMPORTANT: Do NOT modify this file directly.
*
* To change the definition of the flags, edit
* packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js.
*
* To regenerate this code, run the following script from the repo root:
* yarn featureflags --update
*/
#pragma once
#include <folly/dynamic.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
namespace facebook::react {
/**
* This class is a ReactNativeFeatureFlags provider that takes the values for
* feature flags from a folly::dynamic object (e.g. from a JSON object), if
* they are defined. For the flags not defined in the object, it falls back to
* the default values defined in ReactNativeFeatureFlagsDefaults.
*
* The API is strict about typing. It ignores null values from the
* folly::dynamic object, but if the key is defined, the value must have the
* correct type or otherwise throws an exception.
*/
class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDefaults {
private:
folly::dynamic values_;
public:
ReactNativeFeatureFlagsDynamicProvider(folly::dynamic values): values_(std::move(values)) {
if (!values_.isObject()) {
throw std::invalid_argument("ReactNativeFeatureFlagsDynamicProvider: values must be an object");
}
}
bool commonTestFlag() override {
auto value = values_["commonTestFlag"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::commonTestFlag();
}
bool completeReactInstanceCreationOnBgThreadOnAndroid() override {
auto value = values_["completeReactInstanceCreationOnBgThreadOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::completeReactInstanceCreationOnBgThreadOnAndroid();
}
bool disableEventLoopOnBridgeless() override {
auto value = values_["disableEventLoopOnBridgeless"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::disableEventLoopOnBridgeless();
}
bool disableMountItemReorderingAndroid() override {
auto value = values_["disableMountItemReorderingAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::disableMountItemReorderingAndroid();
}
bool enableAlignItemsBaselineOnFabricIOS() override {
auto value = values_["enableAlignItemsBaselineOnFabricIOS"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableAlignItemsBaselineOnFabricIOS();
}
bool enableAndroidLineHeightCentering() override {
auto value = values_["enableAndroidLineHeightCentering"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableAndroidLineHeightCentering();
}
bool enableBridgelessArchitecture() override {
auto value = values_["enableBridgelessArchitecture"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture();
}
bool enableCppPropsIteratorSetter() override {
auto value = values_["enableCppPropsIteratorSetter"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableCppPropsIteratorSetter();
}
bool enableDeletionOfUnmountedViews() override {
auto value = values_["enableDeletionOfUnmountedViews"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableDeletionOfUnmountedViews();
}
bool enableEagerRootViewAttachment() override {
auto value = values_["enableEagerRootViewAttachment"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableEagerRootViewAttachment();
}
bool enableEventEmitterRetentionDuringGesturesOnAndroid() override {
auto value = values_["enableEventEmitterRetentionDuringGesturesOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableEventEmitterRetentionDuringGesturesOnAndroid();
}
bool enableFabricLogs() override {
auto value = values_["enableFabricLogs"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableFabricLogs();
}
bool enableFabricRenderer() override {
auto value = values_["enableFabricRenderer"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableFabricRenderer();
}
bool enableFabricRendererExclusively() override {
auto value = values_["enableFabricRendererExclusively"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableFabricRendererExclusively();
}
bool enableFixForViewCommandRace() override {
auto value = values_["enableFixForViewCommandRace"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableFixForViewCommandRace();
}
bool enableGranularShadowTreeStateReconciliation() override {
auto value = values_["enableGranularShadowTreeStateReconciliation"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableGranularShadowTreeStateReconciliation();
}
bool enableIOSViewClipToPaddingBox() override {
auto value = values_["enableIOSViewClipToPaddingBox"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableIOSViewClipToPaddingBox();
}
bool enableImagePrefetchingAndroid() override {
auto value = values_["enableImagePrefetchingAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableImagePrefetchingAndroid();
}
bool enableLayoutAnimationsOnAndroid() override {
auto value = values_["enableLayoutAnimationsOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableLayoutAnimationsOnAndroid();
}
bool enableLayoutAnimationsOnIOS() override {
auto value = values_["enableLayoutAnimationsOnIOS"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableLayoutAnimationsOnIOS();
}
bool enableLongTaskAPI() override {
auto value = values_["enableLongTaskAPI"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableLongTaskAPI();
}
bool enableNewBackgroundAndBorderDrawables() override {
auto value = values_["enableNewBackgroundAndBorderDrawables"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableNewBackgroundAndBorderDrawables();
}
bool enablePreciseSchedulingForPremountItemsOnAndroid() override {
auto value = values_["enablePreciseSchedulingForPremountItemsOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enablePreciseSchedulingForPremountItemsOnAndroid();
}
bool enablePropsUpdateReconciliationAndroid() override {
auto value = values_["enablePropsUpdateReconciliationAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enablePropsUpdateReconciliationAndroid();
}
bool enableReportEventPaintTime() override {
auto value = values_["enableReportEventPaintTime"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableReportEventPaintTime();
}
bool enableSynchronousStateUpdates() override {
auto value = values_["enableSynchronousStateUpdates"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableSynchronousStateUpdates();
}
bool enableUIConsistency() override {
auto value = values_["enableUIConsistency"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableUIConsistency();
}
bool enableViewRecycling() override {
auto value = values_["enableViewRecycling"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableViewRecycling();
}
bool excludeYogaFromRawProps() override {
auto value = values_["excludeYogaFromRawProps"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::excludeYogaFromRawProps();
}
bool fixDifferentiatorEmittingUpdatesWithWrongParentTag() override {
auto value = values_["fixDifferentiatorEmittingUpdatesWithWrongParentTag"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::fixDifferentiatorEmittingUpdatesWithWrongParentTag();
}
bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override {
auto value = values_["fixMappingOfEventPrioritiesBetweenFabricAndReact"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact();
}
bool fixMountingCoordinatorReportedPendingTransactionsOnAndroid() override {
auto value = values_["fixMountingCoordinatorReportedPendingTransactionsOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::fixMountingCoordinatorReportedPendingTransactionsOnAndroid();
}
bool fuseboxEnabledDebug() override {
auto value = values_["fuseboxEnabledDebug"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::fuseboxEnabledDebug();
}
bool fuseboxEnabledRelease() override {
auto value = values_["fuseboxEnabledRelease"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease();
}
bool initEagerTurboModulesOnNativeModulesQueueAndroid() override {
auto value = values_["initEagerTurboModulesOnNativeModulesQueueAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::initEagerTurboModulesOnNativeModulesQueueAndroid();
}
bool lazyAnimationCallbacks() override {
auto value = values_["lazyAnimationCallbacks"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::lazyAnimationCallbacks();
}
bool loadVectorDrawablesOnImages() override {
auto value = values_["loadVectorDrawablesOnImages"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::loadVectorDrawablesOnImages();
}
bool traceTurboModulePromiseRejectionsOnAndroid() override {
auto value = values_["traceTurboModulePromiseRejectionsOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::traceTurboModulePromiseRejectionsOnAndroid();
}
bool useAlwaysAvailableJSErrorHandling() override {
auto value = values_["useAlwaysAvailableJSErrorHandling"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useAlwaysAvailableJSErrorHandling();
}
bool useFabricInterop() override {
auto value = values_["useFabricInterop"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useFabricInterop();
}
bool useImmediateExecutorInAndroidBridgeless() override {
auto value = values_["useImmediateExecutorInAndroidBridgeless"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useImmediateExecutorInAndroidBridgeless();
}
bool useNativeViewConfigsInBridgelessMode() override {
auto value = values_["useNativeViewConfigsInBridgelessMode"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useNativeViewConfigsInBridgelessMode();
}
bool useOptimisedViewPreallocationOnAndroid() override {
auto value = values_["useOptimisedViewPreallocationOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useOptimisedViewPreallocationOnAndroid();
}
bool useOptimizedEventBatchingOnAndroid() override {
auto value = values_["useOptimizedEventBatchingOnAndroid"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useOptimizedEventBatchingOnAndroid();
}
bool useRuntimeShadowNodeReferenceUpdate() override {
auto value = values_["useRuntimeShadowNodeReferenceUpdate"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useRuntimeShadowNodeReferenceUpdate();
}
bool useTurboModuleInterop() override {
auto value = values_["useTurboModuleInterop"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useTurboModuleInterop();
}
bool useTurboModules() override {
auto value = values_["useTurboModules"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::useTurboModules();
}
};
} // namespace facebook::react
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<8f8c7a372cdf9a3c06bc0d71f1ed85ad>>
* @generated SignedSource<<e5d1c60102f7444332bd34627c02eddd>>
*/
/**
@@ -54,6 +54,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool enableUIConsistency() = 0;
virtual bool enableViewRecycling() = 0;
virtual bool excludeYogaFromRawProps() = 0;
virtual bool fixDifferentiatorEmittingUpdatesWithWrongParentTag() = 0;
virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0;
virtual bool fixMountingCoordinatorReportedPendingTransactionsOnAndroid() = 0;
virtual bool fuseboxEnabledDebug() = 0;
@@ -0,0 +1,78 @@
/*
* 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 <gtest/gtest.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h>
namespace facebook::react {
class ReactNativeFeatureFlagsDynamicProviderTest : public testing::Test {
protected:
void TearDown() override {
ReactNativeFeatureFlags::dangerouslyReset();
}
};
TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, providesDefaults) {
auto values = folly::dynamic::object();
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(
std::move(values)));
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false);
}
TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, providesDynamicOverrides) {
folly::dynamic values = folly::dynamic::object();
values["commonTestFlag"] = true;
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(values));
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
}
TEST_F(
ReactNativeFeatureFlagsDynamicProviderTest,
throwsWithIncorrectFlagTypes) {
folly::dynamic values = folly::dynamic::object();
values["commonTestFlag"] = 12;
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(values));
try {
ReactNativeFeatureFlags::commonTestFlag();
FAIL()
<< "Expected ReactNativeFeatureFlags::commonTestFlag() to throw an exception";
} catch (const std::runtime_error& e) {
EXPECT_STREQ(
"TypeError: expected dynamic type 'boolean', but had type 'int64'",
e.what());
}
}
TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, throwsWithNonObjectValues) {
folly::dynamic values = folly::dynamic("string");
try {
auto provider =
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(values);
FAIL()
<< "Expected ReactNativeFeatureFlagsDynamicProvider constructor to throw an exception";
} catch (const std::invalid_argument& e) {
EXPECT_STREQ(
"ReactNativeFeatureFlagsDynamicProvider: values must be an object",
e.what());
}
}
} // namespace facebook::react
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4808c1455f8e17c42036055dd6a81d7d>>
* @generated SignedSource<<4055a9b5e34ff6740a99d4e08853fe7d>>
*/
/**
@@ -189,6 +189,11 @@ bool NativeReactNativeFeatureFlags::excludeYogaFromRawProps(
return ReactNativeFeatureFlags::excludeYogaFromRawProps();
}
bool NativeReactNativeFeatureFlags::fixDifferentiatorEmittingUpdatesWithWrongParentTag(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::fixDifferentiatorEmittingUpdatesWithWrongParentTag();
}
bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact();
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<6b4909879b76908792d89e3f24c1453a>>
* @generated SignedSource<<9541abe6da92d991557ca6d2f1e36a9a>>
*/
/**
@@ -95,6 +95,8 @@ class NativeReactNativeFeatureFlags
bool excludeYogaFromRawProps(jsi::Runtime& runtime);
bool fixDifferentiatorEmittingUpdatesWithWrongParentTag(jsi::Runtime& runtime);
bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime);
bool fixMountingCoordinatorReportedPendingTransactionsOnAndroid(jsi::Runtime& runtime);
@@ -93,14 +93,11 @@ NativeIntersectionObserver::convertToNativeModuleEntry(
entry.rootRect.origin.y,
entry.rootRect.size.width,
entry.rootRect.size.height};
std::optional<RectAsTuple> intersectionRect;
if (entry.intersectionRect) {
intersectionRect = {
entry.intersectionRect.value().origin.x,
entry.intersectionRect.value().origin.y,
entry.intersectionRect.value().size.width,
entry.intersectionRect.value().size.height};
}
RectAsTuple intersectionRect = {
entry.intersectionRect.origin.x,
entry.intersectionRect.origin.y,
entry.intersectionRect.size.width,
entry.intersectionRect.size.height};
NativeIntersectionObserverEntry nativeModuleEntry = {
entry.intersectionObserverId,
@@ -144,35 +144,6 @@ std::tuple<double, double> NativePerformance::measureWithResult(
return std::tuple{entry.startTime, entry.duration};
}
void NativePerformance::mark(
jsi::Runtime& rt,
std::string name,
double startTime) {
auto [trackName, eventName] = parseTrackName(name);
ReactPerfLogger::mark(eventName, startTime, trackName);
PerformanceEntryReporter::getInstance()->reportMark(name, startTime);
}
void NativePerformance::measure(
jsi::Runtime& rt,
std::string name,
double startTime,
double endTime,
std::optional<double> duration,
std::optional<std::string> startMark,
std::optional<std::string> endMark) {
auto [trackName, eventName] = parseTrackName(name);
// TODO T190600850 support startMark/endMark
if (!startMark && !endMark) {
ReactPerfLogger::measure(eventName, startTime, endTime, trackName);
}
PerformanceEntryReporter::getInstance()->reportMeasure(
eventName, startTime, endTime, duration, startMark, endMark);
}
void NativePerformance::clearMarks(
jsi::Runtime& /*rt*/,
std::optional<std::string> entryName) {
@@ -69,21 +69,6 @@ class NativePerformance : public NativePerformanceCxxSpec<NativePerformance> {
#pragma mark - User Timing Level 3 functions (https://w3c.github.io/user-timing/)
// https://w3c.github.io/user-timing/#mark-method
// TODO delete when `markWithResult` is fully rolled out
void mark(jsi::Runtime& rt, std::string name, double startTime);
// https://w3c.github.io/user-timing/#measure-method
// TODO delete when `measureWithResult` is fully rolled out
void measure(
jsi::Runtime& rt,
std::string name,
double startTime,
double endTime,
std::optional<double> duration,
std::optional<std::string> startMark,
std::optional<std::string> endMark);
// https://w3c.github.io/user-timing/#mark-method
double markWithResult(
jsi::Runtime& rt,
@@ -56,7 +56,7 @@ void LayoutAnimationDriver::animationMutationsForFrame(
// Create the mutation instruction
mutationsList.emplace_back(ShadowViewMutation::UpdateMutation(
keyframe.viewPrev, mutatedShadowView, keyframe.parentView));
keyframe.viewPrev, mutatedShadowView, keyframe.parentTag));
PrintMutationInstruction("Animation Progress:", mutationsList.back());
@@ -13,7 +13,7 @@
#include <react/debug/flags.h>
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/animations/conversions.h>
#include <react/renderer/animations/utils.h>
#include <react/renderer/components/image/ImageProps.h>
@@ -392,6 +392,14 @@ LayoutAnimationKeyFrameManager::pullTransaction(
if (keyframe.type == AnimationConfigurationType::Update &&
mutation.newChildShadowView.tag > 0) {
keyframe.viewPrev = mutation.newChildShadowView;
if (ReactNativeFeatureFlags::
fixDifferentiatorEmittingUpdatesWithWrongParentTag()) {
keyframe.parentTag = mutation.parentTag;
react_native_assert(
keyframe.finalMutationsForKeyFrame.size() == 1);
keyframe.finalMutationsForKeyFrame[0].parentTag =
mutation.parentTag;
}
}
}
}
@@ -418,7 +426,7 @@ LayoutAnimationKeyFrameManager::pullTransaction(
mutation.oldChildShadowView.tag > 0) {
executeMutationImmediately =
ShadowViewMutation::RemoveMutation(
mutation.parentShadowView,
mutation.parentTag,
keyframe.viewPrev,
mutation.index);
}
@@ -441,9 +449,9 @@ LayoutAnimationKeyFrameManager::pullTransaction(
? mutation.newChildShadowView
: viewStart);
react_native_assert(viewFinal.tag > 0);
ShadowView parent = mutation.parentShadowView;
Tag parentTag = mutation.parentTag;
react_native_assert(
parent.tag > 0 ||
parentTag > 0 ||
mutation.type == ShadowViewMutation::Type::Update ||
mutation.type == ShadowViewMutation::Type::Delete);
Tag tag = viewStart.tag;
@@ -499,7 +507,7 @@ LayoutAnimationKeyFrameManager::pullTransaction(
/* .finalMutationsForKeyFrame = */ {},
/* .type = */ AnimationConfigurationType::Create,
/* .tag = */ tag,
/* .parentView = */ parent,
/* .parentTag = */ parentTag,
/* .viewStart = */ viewStart,
/* .viewEnd = */ viewFinal,
/* .viewPrev = */ baselineShadowView,
@@ -537,7 +545,7 @@ LayoutAnimationKeyFrameManager::pullTransaction(
/* .finalMutationsForKeyFrame = */ {mutation},
/* .type = */ AnimationConfigurationType::Update,
/* .tag = */ tag,
/* .parentView = */ parent,
/* .parentTag = */ parentTag,
/* .viewStart = */ viewStart,
/* .viewEnd = */ viewFinal,
/* .viewPrev = */ baselineShadowView,
@@ -623,7 +631,7 @@ LayoutAnimationKeyFrameManager::pullTransaction(
/* .finalMutationsForKeyFrame */ {mutation, deleteMutation},
/* .type */ AnimationConfigurationType::Delete,
/* .tag */ tag,
/* .parentView */ parent,
/* .parentTag */ parentTag,
/* .viewStart */ viewStart,
/* .viewEnd */ viewFinal,
/* .viewPrev */ baselineShadowView,
@@ -1176,19 +1184,17 @@ void LayoutAnimationKeyFrameManager::queueFinalMutationsForCompletedKeyFrame(
break;
case ShadowViewMutation::Type::Insert:
mutationsList.push_back(ShadowViewMutation::InsertMutation(
finalMutation.parentShadowView,
finalMutation.parentTag,
finalMutation.newChildShadowView,
finalMutation.index));
break;
case ShadowViewMutation::Type::Remove:
mutationsList.push_back(ShadowViewMutation::RemoveMutation(
finalMutation.parentShadowView, prev, finalMutation.index));
finalMutation.parentTag, prev, finalMutation.index));
break;
case ShadowViewMutation::Type::Update:
mutationsList.push_back(ShadowViewMutation::UpdateMutation(
prev,
finalMutation.newChildShadowView,
finalMutation.parentShadowView));
prev, finalMutation.newChildShadowView, finalMutation.parentTag));
break;
}
if (finalMutation.newChildShadowView.tag > 0) {
@@ -1213,7 +1219,7 @@ void LayoutAnimationKeyFrameManager::queueFinalMutationsForCompletedKeyFrame(
auto mutatedShadowView =
createInterpolatedShadowView(1, keyframe.viewStart, keyframe.viewEnd);
auto generatedPenultimateMutation = ShadowViewMutation::UpdateMutation(
keyframe.viewPrev, mutatedShadowView, keyframe.parentView);
keyframe.viewPrev, mutatedShadowView, keyframe.parentTag);
react_native_assert(
generatedPenultimateMutation.oldChildShadowView.tag > 0);
react_native_assert(
@@ -1224,7 +1230,7 @@ void LayoutAnimationKeyFrameManager::queueFinalMutationsForCompletedKeyFrame(
mutationsList.push_back(generatedPenultimateMutation);
auto generatedMutation = ShadowViewMutation::UpdateMutation(
mutatedShadowView, keyframe.viewEnd, keyframe.parentView);
mutatedShadowView, keyframe.viewEnd, keyframe.parentTag);
react_native_assert(generatedMutation.oldChildShadowView.tag > 0);
react_native_assert(generatedMutation.newChildShadowView.tag > 0);
PrintMutationInstruction(
@@ -1233,7 +1239,7 @@ void LayoutAnimationKeyFrameManager::queueFinalMutationsForCompletedKeyFrame(
mutationsList.push_back(generatedMutation);
} else {
auto mutation = ShadowViewMutation::UpdateMutation(
keyframe.viewPrev, keyframe.viewEnd, keyframe.parentView);
keyframe.viewPrev, keyframe.viewEnd, keyframe.parentTag);
PrintMutationInstruction(
logPrefix +
"Animation Complete: Queuing up Final Synthetic Mutation:",
@@ -1292,7 +1298,7 @@ void LayoutAnimationKeyFrameManager::
// Detect if they're in the same view hierarchy, but not equivalent
// We've already detected direct conflicts and removed them.
if (animatedKeyFrame.parentView.tag != mutation.parentShadowView.tag) {
if (animatedKeyFrame.parentTag != mutation.parentTag) {
continue;
}
@@ -1397,7 +1403,7 @@ void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation(
// Detect if they're in the same view hierarchy, but not equivalent
// (We've already detected direct conflicts and handled them above)
if (animatedKeyFrame.parentView.tag != mutation.parentShadowView.tag) {
if (animatedKeyFrame.parentTag != mutation.parentTag) {
continue;
}
@@ -1511,8 +1517,8 @@ void LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations(
// we need to force deletion/removal to happen immediately.
bool conflicting = animatedKeyFrame.tag == baselineTag ||
(mutationIsCreateOrDelete &&
animatedKeyFrame.parentView.tag == baselineTag &&
animatedKeyFrame.parentView.tag != 0);
animatedKeyFrame.parentTag == baselineTag &&
animatedKeyFrame.parentTag != 0);
// Conflicting animation detected: if we're mutating a tag under
// animation, or deleting the parent of a tag under animation, or
@@ -70,7 +70,7 @@ struct AnimationKeyFrame {
// Tag representing the node being animated.
Tag tag;
ShadowView parentView;
Tag parentTag;
// ShadowView representing the start and end points of this animation.
ShadowView viewStart;
@@ -20,8 +20,7 @@ static inline bool shouldFirstComeBeforeSecondRemovesOnly(
// come first.
return (lhs.type == ShadowViewMutation::Type::Remove &&
lhs.type == rhs.type) &&
(lhs.parentShadowView.tag == rhs.parentShadowView.tag) &&
(lhs.index > rhs.index);
(lhs.parentTag == rhs.parentTag) && (lhs.index > rhs.index);
}
static inline void handleShouldFirstComeBeforeSecondRemovesOnly(
@@ -31,7 +30,7 @@ static inline void handleShouldFirstComeBeforeSecondRemovesOnly(
ShadowViewMutation::List finalList;
for (auto& mutation : list) {
if (mutation.type == ShadowViewMutation::Type::Remove) {
auto key = std::to_string(mutation.parentShadowView.tag);
auto key = std::to_string(mutation.parentTag);
removeMutationsByTag[key].push_back(mutation);
} else {
finalList.push_back(mutation);
@@ -104,7 +103,7 @@ static inline bool shouldFirstComeBeforeSecondMutation(
// Make sure that removes on the same level are sorted - highest indices
// must come first.
if (lhs.type == ShadowViewMutation::Type::Remove &&
lhs.parentShadowView.tag == rhs.parentShadowView.tag) {
lhs.parentTag == rhs.parentTag) {
if (lhs.index > rhs.index) {
return true;
} else {
@@ -8,12 +8,31 @@
#include "ImageComponentDescriptor.h"
#include <react/renderer/imagemanager/ImageManager.h>
namespace {
std::shared_ptr<facebook::react::ImageManager> getImageManager(
std::shared_ptr<const facebook::react::ContextContainer>&
contextContainer) {
if (auto imageManager =
contextContainer
->find<std::shared_ptr<facebook::react::ImageManager>>(
facebook::react::ImageManagerKey);
imageManager.has_value()) {
return imageManager.value();
}
return std::make_shared<facebook::react::ImageManager>(contextContainer);
}
} // namespace
namespace facebook::react {
extern const char ImageManagerKey[] = "ImageManager";
ImageComponentDescriptor::ImageComponentDescriptor(
const ComponentDescriptorParameters& parameters)
: ConcreteComponentDescriptor(parameters),
imageManager_(std::make_shared<ImageManager>(contextContainer_)){};
imageManager_(getImageManager(contextContainer_)){};
void ImageComponentDescriptor::adopt(ShadowNode& shadowNode) const {
ConcreteComponentDescriptor::adopt(shadowNode);
@@ -14,6 +14,8 @@ namespace facebook::react {
class ImageManager;
extern const char ImageManagerKey[];
/*
* Descriptor for <Image> component.
*/
@@ -32,9 +32,11 @@ target_include_directories(react_render_imagemanager
target_link_libraries(react_render_imagemanager
folly_runtime
mapbufferjni
react_debug
react_render_core
react_render_debug
react_render_graphics
react_render_mounting
reactnativejni
yoga)
@@ -0,0 +1,43 @@
/*
* 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 "ImageFetcher.h"
#include <react/renderer/imagemanager/conversions.h>
namespace facebook::react {
ImageRequest ImageFetcher::requestImage(
const ImageSource& imageSource,
const ImageRequestParams& imageRequestParams,
SurfaceId surfaceId,
Tag tag) const {
auto fabricUIManager_ =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
static auto requestImage =
fabricUIManager_->getClass()
->getMethod<void(
std::string, SurfaceId, Tag, JReadableMapBuffer::javaobject)>(
"experimental_prefetchResource");
auto serializedImageRequest =
serializeImageRequest(imageSource, imageRequestParams);
auto readableMapBuffer =
JReadableMapBuffer::createWithContents(std::move(serializedImageRequest));
requestImage(
fabricUIManager_,
"RCTImageView",
surfaceId,
tag,
readableMapBuffer.get());
auto telemetry = std::make_shared<ImageTelemetry>(surfaceId);
return {imageSource, telemetry};
}
} // namespace facebook::react
@@ -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.
*/
#pragma once
#include <fbjni/fbjni.h>
#include <react/common/mapbuffer/JReadableMapBuffer.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/renderer/imagemanager/ImageRequest.h>
#include <react/renderer/imagemanager/ImageRequestParams.h>
#include <react/utils/ContextContainer.h>
#include <utility>
namespace facebook::react {
class ImageFetcher {
public:
ImageFetcher(ContextContainer::Shared contextContainer)
: contextContainer_(std::move(contextContainer)) {}
ImageRequest requestImage(
const ImageSource& imageSource,
const ImageRequestParams& imageRequestParams,
SurfaceId surfaceId,
Tag tag) const;
private:
ContextContainer::Shared contextContainer_;
};
} // namespace facebook::react
@@ -7,16 +7,18 @@
#include "ImageManager.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include "ImageFetcher.h"
namespace facebook::react {
ImageManager::ImageManager(
const ContextContainer::Shared& /*contextContainer*/) {
// Silence unused-private-field warning.
(void)self_;
// Not implemented.
}
ImageManager::ImageManager(const ContextContainer::Shared& contextContainer)
: self_(new ImageFetcher(contextContainer)) {}
ImageManager::~ImageManager() = default;
ImageManager::~ImageManager() {
// @lint-ignore CLANGTIDY cppcoreguidelines-no-malloc
free(self_);
}
ImageRequest ImageManager::requestImage(
const ImageSource& imageSource,
@@ -26,10 +28,16 @@ ImageRequest ImageManager::requestImage(
ImageRequest ImageManager::requestImage(
const ImageSource& imageSource,
SurfaceId /*surfaceId*/,
const ImageRequestParams& /*imageRequestParams*/,
Tag /* tag */) const {
return {imageSource, nullptr, {}};
SurfaceId surfaceId,
const ImageRequestParams& imageRequestParams,
Tag tag) const {
if (ReactNativeFeatureFlags::enableImagePrefetchingAndroid()) {
// @lint-ignore CLANGTIDY cppcoreguidelines-pro-type-cstyle-cast
return ((ImageFetcher*)self_)
->requestImage(imageSource, imageRequestParams, surfaceId, tag);
} else {
return {imageSource, nullptr, {}};
}
}
} // namespace facebook::react
@@ -50,6 +50,33 @@ bool ShadowViewNodePair::operator!=(const ShadowViewNodePair& rhs) const {
return !(*this == rhs);
}
#ifdef DEBUG_LOGS_DIFFER
static std::ostream& operator<<(
std::ostream& out,
const ShadowViewNodePair& pair) {
out << pair.shadowView.tag;
if (!pair.isConcreteView) {
out << '\'';
}
if (pair.flattened) {
out << '*';
}
return out;
}
static std::ostream& operator<<(
std::ostream& out,
std::vector<ShadowViewNodePair*> vec) {
for (int i = 0; i < vec.size(); i++) {
if (i > 0) {
out << ", ";
}
out << *vec[i];
}
return out;
}
#endif
/*
* Extremely simple and naive implementation of a map.
* The map is simple but it's optimized for particular constraints that we have
@@ -182,6 +209,21 @@ class TinyMap final {
size_t erasedAtFront_{0};
};
#ifdef DEBUG_LOGS_DIFFER
template <typename KeyT, typename ValueT>
static std::ostream& operator<<(std::ostream& out, TinyMap<KeyT, ValueT>& map) {
auto it = map.begin();
if (it != map.end()) {
out << *it->second;
++it;
}
for (; it != map.end(); ++it) {
out << ", " << *it->second;
}
return out;
}
#endif
/*
* Sorting comparator for `reorderInPlaceIfNeeded`.
*/
@@ -365,7 +407,7 @@ static_assert(
static void calculateShadowViewMutations(
ViewNodePairScope& scope,
ShadowViewMutation::List& mutations,
const ShadowView& parentShadowView,
Tag parentTag,
std::vector<ShadowViewNodePair*>&& oldChildPairs,
std::vector<ShadowViewNodePair*>&& newChildPairs);
@@ -384,7 +426,7 @@ static void updateMatchedPairSubtrees(
OrderedMutationInstructionContainer& mutationContainer,
TinyMap<Tag, ShadowViewNodePair*>& newRemainingPairs,
std::vector<ShadowViewNodePair*>& oldChildPairs,
const ShadowView& parentShadowView,
Tag parentTag,
const ShadowViewNodePair& oldPair,
const ShadowViewNodePair& newPair);
@@ -392,7 +434,7 @@ static void updateMatchedPair(
OrderedMutationInstructionContainer& mutationContainer,
bool oldNodeFoundInOrder,
bool newNodeFoundInOrder,
const ShadowView& parentShadowView,
Tag parentTag,
const ShadowViewNodePair& oldPair,
const ShadowViewNodePair& newPair);
@@ -400,9 +442,10 @@ static void calculateShadowViewMutationsFlattener(
ViewNodePairScope& scope,
ReparentMode reparentMode,
OrderedMutationInstructionContainer& mutationContainer,
const ShadowView& parentShadowView,
Tag parentTag,
TinyMap<Tag, ShadowViewNodePair*>& unvisitedOtherNodes,
const ShadowViewNodePair& node,
Tag parentTagForUpdate,
TinyMap<Tag, ShadowViewNodePair*>* parentSubVisitedOtherNewNodes = nullptr,
TinyMap<Tag, ShadowViewNodePair*>* parentSubVisitedOtherOldNodes = nullptr);
@@ -419,7 +462,7 @@ static void updateMatchedPairSubtrees(
OrderedMutationInstructionContainer& mutationContainer,
TinyMap<Tag, ShadowViewNodePair*>& newRemainingPairs,
std::vector<ShadowViewNodePair*>& oldChildPairs,
const ShadowView& parentShadowView,
Tag parentTag,
const ShadowViewNodePair& oldPair,
const ShadowViewNodePair& newPair) {
// Are we flattening or unflattening either one? If node was
@@ -431,11 +474,10 @@ static void updateMatchedPairSubtrees(
// We are either flattening or unflattening this node.
if (oldPair.flattened != newPair.flattened) {
DEBUG_LOGS({
LOG(ERROR)
<< "Differ: flattening or unflattening in updateMatchedPairSubtrees: ["
<< oldPair.shadowView.tag << "] [" << newPair.shadowView.tag << "] "
<< oldPair.flattened << " " << newPair.flattened << " with parent: ["
<< parentShadowView.tag << "]";
LOG(ERROR) << "Differ: "
<< (newPair.flattened ? "flattening" : "unflattening")
<< " in updateMatchedPairSubtrees: " << oldPair << " and "
<< newPair << " with parent [" << parentTag << "]";
});
// Flattening
@@ -448,9 +490,10 @@ static void updateMatchedPairSubtrees(
scope,
ReparentMode::Flatten,
mutationContainer,
parentShadowView,
parentTag,
newRemainingPairs,
oldPair);
oldPair,
oldPair.shadowView.tag);
}
// Unflattening
else {
@@ -478,9 +521,10 @@ static void updateMatchedPairSubtrees(
scope,
ReparentMode::Unflatten,
mutationContainer,
parentShadowView,
parentTag,
unvisitedOldChildPairs,
newPair);
newPair,
parentTag);
// If old nodes were not visited, we know that we can delete
// them now. They will be removed from the hierarchy by the
@@ -519,7 +563,7 @@ static void updateMatchedPairSubtrees(
*(newGrandChildPairsSize != 0u
? &mutationContainer.downwardMutations
: &mutationContainer.destructiveDownwardMutations),
oldPair.shadowView,
oldPair.shadowView.tag,
std::move(oldGrandChildPairs),
std::move(newGrandChildPairs));
}
@@ -537,7 +581,7 @@ static void updateMatchedPair(
OrderedMutationInstructionContainer& mutationContainer,
bool oldNodeFoundInOrder,
bool newNodeFoundInOrder,
const ShadowView& parentShadowView,
Tag parentTag,
const ShadowViewNodePair& oldPair,
const ShadowViewNodePair& newPair) {
oldPair.otherTreePair = &newPair;
@@ -550,7 +594,7 @@ static void updateMatchedPair(
if (newNodeFoundInOrder) {
mutationContainer.insertMutations.push_back(
ShadowViewMutation::InsertMutation(
parentShadowView,
parentTag,
newPair.shadowView,
static_cast<int>(newPair.mountIndex)));
}
@@ -560,7 +604,7 @@ static void updateMatchedPair(
if (oldNodeFoundInOrder) {
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
parentShadowView,
parentTag,
oldPair.shadowView,
static_cast<int>(oldPair.mountIndex)));
}
@@ -573,7 +617,7 @@ static void updateMatchedPair(
if (oldNodeFoundInOrder && !newNodeFoundInOrder) {
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
parentShadowView,
parentTag,
newPair.shadowView,
static_cast<int>(oldPair.mountIndex)));
}
@@ -584,7 +628,7 @@ static void updateMatchedPair(
if (oldPair.shadowView != newPair.shadowView) {
mutationContainer.updateMutations.push_back(
ShadowViewMutation::UpdateMutation(
oldPair.shadowView, newPair.shadowView, parentShadowView));
oldPair.shadowView, newPair.shadowView, parentTag));
}
}
}
@@ -621,52 +665,32 @@ static void updateMatchedPair(
* performed in the subtree. If it *is* in the map, it means the node is not
* in the Tree, and should be Deleted/Created **after this function is
* called**, by the caller.
*
* @param parentTag parent under which nodes should be mounted/unmounted
* @param parentTagForUpdate current parent in which node is mounted,
* used for update mutations
*/
static void calculateShadowViewMutationsFlattener(
ViewNodePairScope& scope,
ReparentMode reparentMode,
OrderedMutationInstructionContainer& mutationContainer,
const ShadowView& parentShadowView,
Tag parentTag,
TinyMap<Tag, ShadowViewNodePair*>& unvisitedOtherNodes,
const ShadowViewNodePair& node,
Tag parentTagForUpdate,
TinyMap<Tag, ShadowViewNodePair*>* parentSubVisitedOtherNewNodes,
TinyMap<Tag, ShadowViewNodePair*>* parentSubVisitedOtherOldNodes) {
DEBUG_LOGS({
LOG(ERROR) << "Differ Flattener 1: "
<< (reparentMode == ReparentMode::Unflatten ? "Unflattening"
: "Flattening")
<< " [" << node.shadowView.tag << "]";
});
// Step 1: iterate through entire tree
std::vector<ShadowViewNodePair*> treeChildren =
sliceChildShadowNodeViewPairsFromViewNodePair(node, scope);
DEBUG_LOGS({
LOG(ERROR) << "Differ Flattener 1.4: "
LOG(ERROR) << "Differ Flattener: "
<< (reparentMode == ReparentMode::Unflatten ? "Unflattening"
: "Flattening")
<< " [" << node.shadowView.tag << "]";
LOG(ERROR) << "Differ Flattener Entry: Child Pairs: ";
std::string strTreeChildPairs;
for (size_t k = 0; k < treeChildren.size(); k++) {
strTreeChildPairs.append(std::to_string(treeChildren[k]->shadowView.tag));
strTreeChildPairs.append(treeChildren[k]->isConcreteView ? "" : "'");
strTreeChildPairs.append(treeChildren[k]->flattened ? "*" : "");
strTreeChildPairs.append(", ");
}
std::string strListChildPairs;
for (auto& unvisitedNode : unvisitedOtherNodes) {
strListChildPairs.append(
std::to_string(unvisitedNode.second->shadowView.tag));
strListChildPairs.append(unvisitedNode.second->isConcreteView ? "" : "'");
strListChildPairs.append(unvisitedNode.second->flattened ? "*" : "");
strListChildPairs.append(", ");
}
LOG(ERROR) << "Differ Flattener Entry: Tree Child Pairs: "
<< strTreeChildPairs;
LOG(ERROR) << "Differ Flattener Entry: List Child Pairs: "
<< strListChildPairs;
LOG(ERROR) << "> Tree Child Pairs: " << treeChildren;
LOG(ERROR) << "> List Child Pairs: " << unvisitedOtherNodes;
});
// Views in other tree that are visited by sub-flattening or
@@ -777,13 +801,13 @@ static void calculateShadowViewMutationsFlattener(
treeChildPair.otherTreePair->isConcreteView) {
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
node.shadowView,
node.shadowView.tag,
treeChildPair.otherTreePair->shadowView,
static_cast<int>(treeChildPair.mountIndex)));
} else {
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
node.shadowView,
node.shadowView.tag,
treeChildPair.shadowView,
static_cast<int>(treeChildPair.mountIndex)));
}
@@ -792,7 +816,7 @@ static void calculateShadowViewMutationsFlattener(
// we can safely insert it without checking in the other tree
mutationContainer.insertMutations.push_back(
ShadowViewMutation::InsertMutation(
node.shadowView,
node.shadowView.tag,
treeChildPair.shadowView,
static_cast<int>(treeChildPair.mountIndex)));
}
@@ -838,11 +862,18 @@ static void calculateShadowViewMutationsFlattener(
// ShadowNode.
if (newTreeNodePair.shadowView != oldTreeNodePair.shadowView &&
newTreeNodePair.isConcreteView && oldTreeNodePair.isConcreteView) {
// We execute updates before creates, so pass the current parent in when
// unflattening.
// TODO: whenever we insert, we already update the relevant properties,
// so this update is redundant. We should remove this.
mutationContainer.updateMutations.push_back(
ShadowViewMutation::UpdateMutation(
oldTreeNodePair.shadowView,
newTreeNodePair.shadowView,
node.shadowView));
ReactNativeFeatureFlags::
fixDifferentiatorEmittingUpdatesWithWrongParentTag()
? parentTagForUpdate
: node.shadowView.tag));
}
// Update children if appropriate.
@@ -852,7 +883,7 @@ static void calculateShadowViewMutationsFlattener(
calculateShadowViewMutations(
innerScope,
mutationContainer.downwardMutations,
newTreeNodePair.shadowView,
newTreeNodePair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
oldTreeNodePair, innerScope),
sliceChildShadowNodeViewPairsFromViewNodePair(
@@ -873,10 +904,13 @@ static void calculateShadowViewMutationsFlattener(
childReparentMode,
mutationContainer,
(reparentMode == ReparentMode::Flatten
? parentShadowView
: newTreeNodePair.shadowView),
? parentTag
: newTreeNodePair.shadowView.tag),
unvisitedOtherNodes,
treeChildPair,
(reparentMode == ReparentMode::Flatten
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
subVisitedOldMap);
} else {
@@ -914,10 +948,13 @@ static void calculateShadowViewMutationsFlattener(
ReparentMode::Flatten,
mutationContainer,
(reparentMode == ReparentMode::Flatten
? parentShadowView
: newTreeNodePair.shadowView),
? parentTag
: newTreeNodePair.shadowView.tag),
unvisitedRecursiveChildPairs,
oldTreeNodePair,
(reparentMode == ReparentMode::Flatten
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
subVisitedOldMap);
}
@@ -929,10 +966,13 @@ static void calculateShadowViewMutationsFlattener(
ReparentMode::Unflatten,
mutationContainer,
(reparentMode == ReparentMode::Flatten
? parentShadowView
: newTreeNodePair.shadowView),
? parentTag
: newTreeNodePair.shadowView.tag),
unvisitedRecursiveChildPairs,
newTreeNodePair,
(reparentMode == ReparentMode::Flatten
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
subVisitedOldMap);
@@ -1028,7 +1068,7 @@ static void calculateShadowViewMutationsFlattener(
calculateShadowViewMutations(
innerScope,
mutationContainer.destructiveDownwardMutations,
treeChildPair.shadowView,
treeChildPair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
treeChildPair, innerScope),
{});
@@ -1042,7 +1082,7 @@ static void calculateShadowViewMutationsFlattener(
calculateShadowViewMutations(
innerScope,
mutationContainer.downwardMutations,
treeChildPair.shadowView,
treeChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
treeChildPair, innerScope));
@@ -1054,7 +1094,7 @@ static void calculateShadowViewMutationsFlattener(
static void calculateShadowViewMutations(
ViewNodePairScope& scope,
ShadowViewMutation::List& mutations,
const ShadowView& parentShadowView,
Tag parentTag,
std::vector<ShadowViewNodePair*>&& oldChildPairs,
std::vector<ShadowViewNodePair*>&& newChildPairs) {
if (oldChildPairs.empty() && newChildPairs.empty()) {
@@ -1067,28 +1107,9 @@ static void calculateShadowViewMutations(
auto mutationContainer = OrderedMutationInstructionContainer{};
DEBUG_LOGS({
LOG(ERROR) << "Differ Entry: Child Pairs of node: [" << parentShadowView.tag
<< "]";
std::string strOldChildPairs;
for (size_t oldIndex = 0; oldIndex < oldChildPairs.size(); oldIndex++) {
strOldChildPairs.append(
std::to_string(oldChildPairs[oldIndex]->shadowView.tag));
strOldChildPairs.append(
oldChildPairs[oldIndex]->isConcreteView ? "" : "'");
strOldChildPairs.append(oldChildPairs[oldIndex]->flattened ? "*" : "");
strOldChildPairs.append(", ");
}
std::string strNewChildPairs;
for (size_t newIndex = 0; newIndex < newChildPairs.size(); newIndex++) {
strNewChildPairs.append(
std::to_string(newChildPairs[newIndex]->shadowView.tag));
strNewChildPairs.append(
newChildPairs[newIndex]->isConcreteView ? "" : "'");
strNewChildPairs.append(newChildPairs[newIndex]->flattened ? "*" : "");
strNewChildPairs.append(", ");
}
LOG(ERROR) << "Differ Entry: Old Child Pairs: " << strOldChildPairs;
LOG(ERROR) << "Differ Entry: New Child Pairs: " << strNewChildPairs;
LOG(ERROR) << "Differ Entry: Child Pairs of node: [" << parentTag << "]";
LOG(ERROR) << "> Old Child Pairs: " << oldChildPairs;
LOG(ERROR) << "> New Child Pairs: " << newChildPairs;
});
// Stage 1: Collecting `Update` mutations
@@ -1102,7 +1123,7 @@ static void calculateShadowViewMutations(
LOG(ERROR) << "Differ Branch 1.1: Tags Different: ["
<< oldChildPair.shadowView.tag << "] ["
<< newChildPair.shadowView.tag << "]" << " with parent: ["
<< parentShadowView.tag << "]";
<< parentTag << "]";
});
// Totally different nodes, updating is impossible.
@@ -1117,23 +1138,16 @@ static void calculateShadowViewMutations(
}
DEBUG_LOGS({
LOG(ERROR) << "Differ Branch 1.2: Same tags, update and recurse: ["
<< oldChildPair.shadowView.tag << "]"
<< (oldChildPair.flattened ? " (flattened)" : "")
<< (oldChildPair.isConcreteView ? " (concrete)" : "") << "["
<< newChildPair.shadowView.tag << "]"
<< (newChildPair.flattened ? " (flattened)" : "")
<< (newChildPair.isConcreteView ? " (concrete)" : "")
<< " with parent: [" << parentShadowView.tag << "]";
LOG(ERROR) << "Differ Branch 1.2: Same tags, update and recurse: "
<< oldChildPair << " and " << newChildPair << " with parent: ["
<< parentTag << "]";
});
if (newChildPair.isConcreteView &&
oldChildPair.shadowView != newChildPair.shadowView) {
mutationContainer.updateMutations.push_back(
ShadowViewMutation::UpdateMutation(
oldChildPair.shadowView,
newChildPair.shadowView,
parentShadowView));
oldChildPair.shadowView, newChildPair.shadowView, parentTag));
}
// Recursively update tree if ShadowNode pointers are not equal
@@ -1150,7 +1164,7 @@ static void calculateShadowViewMutations(
*(newGrandChildPairsSize != 0u
? &mutationContainer.downwardMutations
: &mutationContainer.destructiveDownwardMutations),
oldChildPair.shadowView,
oldChildPair.shadowView.tag,
std::move(oldGrandChildPairs),
std::move(newGrandChildPairs));
}
@@ -1165,9 +1179,8 @@ static void calculateShadowViewMutations(
const auto& oldChildPair = *oldChildPairs[index];
DEBUG_LOGS({
LOG(ERROR) << "Differ Branch 2: Deleting Tag/Tree: ["
<< oldChildPair.shadowView.tag << "]" << " with parent: ["
<< parentShadowView.tag << "]";
LOG(ERROR) << "Differ Branch 2: Deleting Tag/Tree: " << oldChildPair
<< " with parent: [" << parentTag << "]";
});
if (!oldChildPair.isConcreteView) {
@@ -1178,7 +1191,7 @@ static void calculateShadowViewMutations(
ShadowViewMutation::DeleteMutation(oldChildPair.shadowView));
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
parentShadowView,
parentTag,
oldChildPair.shadowView,
static_cast<int>(oldChildPair.mountIndex)));
@@ -1188,7 +1201,7 @@ static void calculateShadowViewMutations(
calculateShadowViewMutations(
innerScope,
mutationContainer.destructiveDownwardMutations,
oldChildPair.shadowView,
oldChildPair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
oldChildPair, innerScope),
{});
@@ -1200,9 +1213,8 @@ static void calculateShadowViewMutations(
const auto& newChildPair = *newChildPairs[index];
DEBUG_LOGS({
LOG(ERROR) << "Differ Branch 3: Creating Tag/Tree: ["
<< newChildPair.shadowView.tag << "]" << " with parent: ["
<< parentShadowView.tag << "]";
LOG(ERROR) << "Differ Branch 3: Creating Tag/Tree: " << newChildPair
<< " with parent: [" << parentTag << "]";
});
if (!newChildPair.isConcreteView) {
@@ -1211,7 +1223,7 @@ static void calculateShadowViewMutations(
mutationContainer.insertMutations.push_back(
ShadowViewMutation::InsertMutation(
parentShadowView,
parentTag,
newChildPair.shadowView,
static_cast<int>(newChildPair.mountIndex)));
mutationContainer.createMutations.push_back(
@@ -1221,7 +1233,7 @@ static void calculateShadowViewMutations(
calculateShadowViewMutations(
innerScope,
mutationContainer.downwardMutations,
newChildPair.shadowView,
newChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
newChildPair, innerScope));
@@ -1257,22 +1269,17 @@ static void calculateShadowViewMutations(
if (newTag == oldTag) {
DEBUG_LOGS({
LOG(ERROR) << "Differ Branch 5: Matched Tags at indices: "
<< oldIndex << " " << newIndex << ": ["
<< oldChildPair.shadowView.tag << "]"
<< (oldChildPair.flattened ? "(flattened)" : "")
<< (oldChildPair.isConcreteView ? "(concrete)" : "")
<< " [" << newChildPair.shadowView.tag << "]"
<< (newChildPair.flattened ? "(flattened)" : "")
<< (newChildPair.isConcreteView ? "(concrete)" : "")
<< " with parent: [" << parentShadowView.tag << "]";
LOG(ERROR) << "Differ Branch 4: Matched Tags at indices: "
<< oldIndex << " and " << newIndex << ": "
<< oldChildPair << " and " << newChildPair
<< " with parent: [" << parentTag << "]";
});
updateMatchedPair(
mutationContainer,
true,
true,
parentShadowView,
parentTag,
oldChildPair,
newChildPair);
@@ -1281,7 +1288,7 @@ static void calculateShadowViewMutations(
mutationContainer,
newRemainingPairs,
oldChildPairs,
parentShadowView,
parentTag,
oldChildPair,
newChildPair);
@@ -1306,11 +1313,18 @@ static void calculateShadowViewMutations(
if (insertedIt != newInsertedPairs.end()) {
const auto& newChildPair = *insertedIt->second;
DEBUG_LOGS({
LOG(ERROR) << "Differ Branch 5: Founded reordered tags at indices: "
<< oldIndex << ": " << oldChildPair << " and "
<< newChildPair << " with parent: ["
<< parentShadowView.tag << "]";
});
updateMatchedPair(
mutationContainer,
true,
false,
parentShadowView,
parentTag,
oldChildPair,
newChildPair);
@@ -1319,7 +1333,7 @@ static void calculateShadowViewMutations(
mutationContainer,
newRemainingPairs,
oldChildPairs,
parentShadowView,
parentTag,
oldChildPair,
newChildPair);
@@ -1345,13 +1359,10 @@ static void calculateShadowViewMutations(
DEBUG_LOGS({
LOG(ERROR)
<< "Differ Branch 9: Removing tag that was not reinserted: "
<< oldIndex << ": [" << oldChildPair.shadowView.tag << "]"
<< (oldChildPair.flattened ? " (flattened)" : "")
<< (oldChildPair.isConcreteView ? " (concrete)" : "")
<< " with parent: [" << parentShadowView.tag << "] "
<< "node is in other tree? "
<< (oldChildPair.inOtherTree() ? "yes" : "no");
<< "Differ Branch 6: Removing tag that was not re-inserted: "
<< oldChildPair << " with parent: [" << parentTag
<< "], which is " << (oldChildPair.inOtherTree() ? "" : "not ")
<< "in other tree";
});
// Edge case: node is not found in `newRemainingPairs`, due to
@@ -1373,7 +1384,7 @@ static void calculateShadowViewMutations(
// hierarchy.
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
parentShadowView,
parentTag,
otherTreeView,
static_cast<int>(oldChildPair.mountIndex)));
continue;
@@ -1381,7 +1392,7 @@ static void calculateShadowViewMutations(
mutationContainer.removeMutations.push_back(
ShadowViewMutation::RemoveMutation(
parentShadowView,
parentTag,
oldChildPair.shadowView,
static_cast<int>(oldChildPair.mountIndex)));
@@ -1398,17 +1409,14 @@ static void calculateShadowViewMutations(
auto& newChildPair = *newChildPairs[newIndex];
DEBUG_LOGS({
LOG(ERROR)
<< "Differ Branch 10: Inserting tag/tree that was not (yet?) removed from hierarchy: "
<< newIndex << "/" << newSize << ": ["
<< newChildPair.shadowView.tag << "]"
<< (newChildPair.flattened ? " (flattened)" : "")
<< (newChildPair.isConcreteView ? " (concrete)" : "")
<< " with parent: [" << parentShadowView.tag << "]";
<< "Differ Branch 7: Inserting tag/tree that was not (yet?) removed from hierarchy: "
<< newChildPair << " @ " << newIndex << "/" << newSize
<< " with parent: [" << parentTag << "]";
});
if (newChildPair.isConcreteView) {
mutationContainer.insertMutations.push_back(
ShadowViewMutation::InsertMutation(
parentShadowView,
parentTag,
newChildPair.shadowView,
static_cast<int>(newChildPair.mountIndex)));
}
@@ -1438,12 +1446,10 @@ static void calculateShadowViewMutations(
DEBUG_LOGS({
LOG(ERROR)
<< "Differ Branch 11: Deleting tag/tree that was not in new hierarchy: "
<< "[" << oldChildPair.shadowView.tag << "]"
<< (oldChildPair.flattened ? "(flattened)" : "")
<< (oldChildPair.isConcreteView ? "(concrete)" : "")
<< "Differ Branch 8: Deleting tag/tree that was not in new hierarchy: "
<< oldChildPair
<< (oldChildPair.inOtherTree() ? "(in other tree)" : "")
<< " with parent: [" << parentShadowView.tag << "] ##"
<< " with parent: [" << parentTag << "] ##"
<< std::hash<ShadowView>{}(oldChildPair.shadowView);
});
@@ -1458,7 +1464,7 @@ static void calculateShadowViewMutations(
calculateShadowViewMutations(
innerScope,
mutationContainer.destructiveDownwardMutations,
oldChildPair.shadowView,
oldChildPair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
oldChildPair, innerScope),
{});
@@ -1480,12 +1486,10 @@ static void calculateShadowViewMutations(
DEBUG_LOGS({
LOG(ERROR)
<< "Differ Branch 12: Inserting tag/tree that was not in old hierarchy: "
<< "[" << newChildPair.shadowView.tag << "]"
<< (newChildPair.flattened ? "(flattened)" : "")
<< (newChildPair.isConcreteView ? "(concrete)" : "")
<< "Differ Branch 9: Inserting tag/tree that was not in old hierarchy: "
<< newChildPair
<< (newChildPair.inOtherTree() ? "(in other tree)" : "")
<< " with parent: [" << parentShadowView.tag << "]";
<< " with parent: [" << parentTag << "]";
});
if (!newChildPair.isConcreteView) {
@@ -1502,7 +1506,7 @@ static void calculateShadowViewMutations(
calculateShadowViewMutations(
innerScope,
mutationContainer.downwardMutations,
newChildPair.shadowView,
newChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
newChildPair, innerScope));
@@ -1567,7 +1571,7 @@ ShadowViewMutation::List calculateShadowViewMutations(
calculateShadowViewMutations(
innerViewNodePairScope,
mutations,
ShadowView(oldRootShadowNode),
oldRootShadowNode.getTag(),
sliceChildShadowNodeViewPairs(
ShadowViewNodePair{.shadowNode = &oldRootShadowNode},
viewNodePairScope),
@@ -1575,6 +1579,38 @@ ShadowViewMutation::List calculateShadowViewMutations(
ShadowViewNodePair{.shadowNode = &newRootShadowNode},
viewNodePairScope));
DEBUG_LOGS({
LOG(ERROR) << "Differ Completed: " << mutations.size() << " mutations";
for (size_t i = 0; i < mutations.size(); i++) {
auto& mutation = mutations[i];
switch (mutation.type) {
case ShadowViewMutation::Type::Create:
LOG(ERROR) << "[" << i << "] CREATE "
<< mutation.newChildShadowView.tag;
break;
case ShadowViewMutation::Type::Delete:
LOG(ERROR) << "[" << i << "] DELETE "
<< mutation.oldChildShadowView.tag;
break;
case ShadowViewMutation::Type::Insert:
LOG(ERROR) << "[" << i << "] INSERT "
<< mutation.newChildShadowView.tag << " INTO "
<< mutation.parentTag << " @ " << mutation.index;
break;
case ShadowViewMutation::Type::Remove:
LOG(ERROR) << "[" << i << "] REMOVE "
<< mutation.oldChildShadowView.tag << " FROM "
<< mutation.parentTag << " @ " << mutation.index;
break;
case ShadowViewMutation::Type::Update:
LOG(ERROR) << "[" << i << "] UPDATE "
<< mutation.newChildShadowView.tag << " IN "
<< mutation.parentTag;
break;
}
}
});
return mutations;
}
@@ -14,7 +14,7 @@ namespace facebook::react {
ShadowViewMutation ShadowViewMutation::CreateMutation(ShadowView shadowView) {
return {
/* .type = */ Create,
/* .parentShadowView = */ {},
/* .parentTag = */ -1,
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(shadowView),
/* .index = */ -1,
@@ -24,7 +24,7 @@ ShadowViewMutation ShadowViewMutation::CreateMutation(ShadowView shadowView) {
ShadowViewMutation ShadowViewMutation::DeleteMutation(ShadowView shadowView) {
return {
/* .type = */ Delete,
/* .parentShadowView = */ {},
/* .parentTag = */ -1,
/* .oldChildShadowView = */ std::move(shadowView),
/* .newChildShadowView = */ {},
/* .index = */ -1,
@@ -32,12 +32,12 @@ ShadowViewMutation ShadowViewMutation::DeleteMutation(ShadowView shadowView) {
}
ShadowViewMutation ShadowViewMutation::InsertMutation(
ShadowView parentShadowView,
Tag parentTag,
ShadowView childShadowView,
int index) {
return {
/* .type = */ Insert,
/* .parentShadowView = */ std::move(parentShadowView),
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(childShadowView),
/* .index = */ index,
@@ -45,12 +45,12 @@ ShadowViewMutation ShadowViewMutation::InsertMutation(
}
ShadowViewMutation ShadowViewMutation::RemoveMutation(
ShadowView parentShadowView,
Tag parentTag,
ShadowView childShadowView,
int index) {
return {
/* .type = */ Remove,
/* .parentShadowView = */ std::move(parentShadowView),
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ std::move(childShadowView),
/* .newChildShadowView = */ {},
/* .index = */ index,
@@ -60,10 +60,10 @@ ShadowViewMutation ShadowViewMutation::RemoveMutation(
ShadowViewMutation ShadowViewMutation::UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
ShadowView parentShadowView) {
Tag parentTag) {
return {
/* .type = */ Update,
/* .parentShadowView = */ std::move(parentShadowView),
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ std::move(oldChildShadowView),
/* .newChildShadowView = */ std::move(newChildShadowView),
/* .index = */ -1,
@@ -88,12 +88,12 @@ bool ShadowViewMutation::mutatedViewIsVirtual() const {
ShadowViewMutation::ShadowViewMutation(
Type type,
ShadowView parentShadowView,
Tag parentTag,
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
int index)
: type(type),
parentShadowView(std::move(parentShadowView)),
parentTag(parentTag),
oldChildShadowView(std::move(oldChildShadowView)),
newChildShadowView(std::move(newChildShadowView)),
index(index) {}
@@ -131,10 +131,10 @@ std::vector<DebugStringConvertibleObject> getDebugProps(
mutation.newChildShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.parentShadowView.componentHandle != 0
mutation.parentTag != -1
? DebugStringConvertibleObject{"parent",
getDebugDescription(
mutation.parentShadowView,
mutation.parentTag,
options)}
: DebugStringConvertibleObject{},
mutation.index != -1
@@ -13,7 +13,7 @@
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.
@@ -26,43 +26,73 @@ struct ShadowViewMutation final {
#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);
/*
/**
* Creates and returns an `Insert` mutation.
*/
static ShadowViewMutation InsertMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index);
static ShadowViewMutation
InsertMutation(Tag parentTag, ShadowView childShadowView, int index);
/*
/**
* Creates and returns an `Insert` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation InsertMutation(
const ShadowView& parentShadowView,
ShadowView childShadowView,
int index) {
return InsertMutation(parentShadowView.tag, childShadowView, index);
}
/**
* Creates and returns a `Remove` mutation.
*/
static ShadowViewMutation RemoveMutation(
ShadowView parentShadowView,
ShadowView childShadowView,
int index);
static ShadowViewMutation
RemoveMutation(Tag parentTag, ShadowView childShadowView, int index);
/*
/**
* Creates and returns a `Remove` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation RemoveMutation(
const ShadowView& parentShadowView,
ShadowView childShadowView,
int index) {
return RemoveMutation(parentShadowView.tag, childShadowView, index);
}
/**
* Creates and returns an `Update` mutation.
*/
static ShadowViewMutation UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
ShadowView parentShadowView);
Tag parentTag);
/**
* Creates and returns an `Update` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
const ShadowView& parentShadowView) {
return UpdateMutation(
oldChildShadowView, newChildShadowView, parentShadowView.tag);
}
#pragma mark - Type
enum Type {
enum Type : std::uint8_t {
Create = 1,
Delete = 2,
Insert = 4,
@@ -73,7 +103,7 @@ struct ShadowViewMutation final {
#pragma mark - Fields
Type type = {Create};
ShadowView parentShadowView = {};
Tag parentTag = -1;
ShadowView oldChildShadowView = {};
ShadowView newChildShadowView = {};
int index = -1;
@@ -88,7 +118,7 @@ struct ShadowViewMutation final {
private:
ShadowViewMutation(
Type type,
ShadowView parentShadowView,
Tag parentTag,
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
int index);
@@ -42,7 +42,7 @@ void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
for (const auto& mutation : mutations) {
switch (mutation.type) {
case ShadowViewMutation::Create: {
react_native_assert(mutation.parentShadowView == ShadowView{});
react_native_assert(mutation.parentTag == -1);
react_native_assert(mutation.oldChildShadowView == ShadowView{});
react_native_assert(mutation.newChildShadowView.props);
auto stubView = std::make_shared<StubView>();
@@ -69,7 +69,7 @@ void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
<< "] ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
});
react_native_assert(mutation.parentShadowView == ShadowView{});
react_native_assert(mutation.parentTag == -1);
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto tag = mutation.oldChildShadowView.tag;
react_native_assert(hasTag(tag));
@@ -98,7 +98,7 @@ void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
case ShadowViewMutation::Insert: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.oldChildShadowView == ShadowView{});
auto parentTag = mutation.parentShadowView.tag;
auto parentTag = mutation.parentTag;
auto childTag = mutation.newChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
@@ -140,7 +140,7 @@ void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
case ShadowViewMutation::Remove: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto parentTag = mutation.parentShadowView.tag;
auto parentTag = mutation.parentTag;
auto childTag = mutation.oldChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
@@ -221,6 +221,10 @@ void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
react_native_assert(hasTag(mutation.newChildShadowView.tag));
auto oldStubView = registry_[mutation.newChildShadowView.tag];
react_native_assert(oldStubView->tag != 0);
if (mutation.parentTag != 0) {
react_native_assert(hasTag(mutation.parentTag));
react_native_assert(oldStubView->parentTag == mutation.parentTag);
}
if ((ShadowView)(*oldStubView) != mutation.oldChildShadowView) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: UPDATE mutation assertion failure: oldChildShadowView does not match oldStubView: ["
@@ -55,7 +55,7 @@ static void calculateShadowViewMutationsForNewTree(
mutations.push_back(
ShadowViewMutation::CreateMutation(newChildPair->shadowView));
mutations.push_back(ShadowViewMutation::InsertMutation(
parentShadowView,
parentShadowView.tag,
newChildPair->shadowView,
static_cast<int>(newChildPair->mountIndex)));
@@ -558,8 +558,7 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
EXPECT_EQ(mutations1[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations1[1].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations1[1].oldChildShadowView.tag, reparentedViewA->getTag());
// This is incorrect! ChildH does not exist yet at this point
EXPECT_EQ(mutations1[1].parentShadowView.tag, childH->getTag());
EXPECT_EQ(mutations1[1].parentTag, childG->getTag());
EXPECT_EQ(mutations1[2].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations1[2].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations1[3].type, ShadowViewMutation::Create);
@@ -574,7 +573,7 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
EXPECT_EQ(mutations2.size(), 5);
EXPECT_EQ(mutations2[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations2[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations2[0].parentShadowView.tag, emptyRootNode->getTag());
EXPECT_EQ(mutations2[0].parentTag, emptyRootNode->getTag());
EXPECT_EQ(mutations2[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations2[1].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations2[2].type, ShadowViewMutation::Remove);
@@ -594,7 +593,7 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
EXPECT_EQ(mutations3.size(), 15);
EXPECT_EQ(mutations3[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations3[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations3[0].parentShadowView.tag, emptyRootNode->getTag());
EXPECT_EQ(mutations3[0].parentTag, emptyRootNode->getTag());
EXPECT_EQ(mutations3[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations3[1].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations3[2].type, ShadowViewMutation::Create);

Some files were not shown because too many files have changed in this diff Show More