Compare commits

...
Author SHA1 Message Date
Nick Gerleman 28beded0d4 Add missing glog dep to React-idlecallbacksnativemodule.podspec 2024-12-12 05:22:39 -08:00
Alex Hunt 9888b499f5 Add support for Performance.mark events in PerformanceTracer (#48200)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48200

Wires up `Performance.mark()` events, completing support for User Timings in Fusebox.

Other changes:

- Refactors `reportMeasure` to receive a `duration`.
- Fixes conversion for time values (ms -> µs) in emitted trace events.

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D66704283

fbshipit-source-id: 352abbade26eb976e793481dde04463431bf2eb7
2024-12-12 04:40:43 -08:00
Alex Hunt fd9cdf3214 Add new Fusebox Tracing API, integrate into PerformanceEntryReporter (2/2) (#48043)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48043

Adds a new `PerformanceTracing` API to replace `ReactPerfLogger` and `FuseboxTracer`.

- Mostly a clone of `FuseboxTracer`, with small refactorings.
- Exposes a new `CdpTracing.h` header, intended for shared CDP/Chrome types (that will later propagate through to the runtime impl of `performance.mark,measure()`).
- These live in a new `jsinspector_tracing` library, to avoid a dependency cycle.

**Key change**: With both diffs, `PerformanceTracer` is added to `PerformanceEntryReporter` to initially wire up the `performance.measure` event — replacing the previous routing.

- `FuseboxTracer` remains load-bearing for the out-of-tree call to `stopTracingAndWriteToFile()`.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D66650181

fbshipit-source-id: 9092257f23cdb8746e69f5ff3eb7dbf4c8142938
2024-12-12 04:40:43 -08:00
Alex Hunt 59969b6135 Remove Fusebox calls from ReactPerfLogger, rename (1/2) (#48044)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48044

Updates `ReactPerfLogger` (now renamed `ReactPerfettoLogger`) to log to Perfetto only.

This precedes integrating `FuseboxTracer` calls into `PerformanceEntryReporter` for User Timing events and Interaction events.

Changelog: [Internal]

Reviewed By: rubennorte

Differential Revision: D66600278

fbshipit-source-id: f250f6b018d091d740bb13d5fcce9b716c4a6f72
2024-12-12 04:40:43 -08:00
Mateo Guzmán 79ed11f8f4 fix(rn-tester): text input and xml http request dark mode adjustments (#48207)
Summary:
Was troubleshooting in these modules recently and noticed a few texts off in dark mode. Replacing a few instances of the Text component in all I could see in the two screens.

## Changelog:

[INTERNAL] - RN Tester `TextInput` & `XMLHttpRequest` dark mode adjustments

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

Test Plan:
<details>
<summary>View Screenshots (before fixes)</summary>

| Screenshot 1 | Screenshot 2 | Screenshot 3 | Screenshot 4 | Screenshot 5 | Screenshot 6 |
|--------------|--------------|--------------|--------------|--------------|--------------|
| ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 11 36](https://github.com/user-attachments/assets/c396bc5d-4b3c-4f63-8ac0-68bc808aa9d4) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 11 21](https://github.com/user-attachments/assets/cd8e5a2f-9066-4bc7-b56b-65dbd6df7dc4) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 30](https://github.com/user-attachments/assets/da451c18-40cd-4421-b8f5-a2462f82fee9) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 23](https://github.com/user-attachments/assets/32925fdb-f983-439f-b898-6986b739895a) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 19](https://github.com/user-attachments/assets/f0db7b49-dfef-4abc-aef0-2d55b016c0f8) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 16](https://github.com/user-attachments/assets/bad6dc45-3679-4bb6-aadd-6743053cd90f) |
| ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 10](https://github.com/user-attachments/assets/43028f51-c6e2-4b26-b20d-a3639606d6d5) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 10 07](https://github.com/user-attachments/assets/f8e2196e-bc64-4507-b330-d1d85730ddff) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 09 59](https://github.com/user-attachments/assets/a7100770-4b38-421f-9b73-09e4fff9e012) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 09 53](https://github.com/user-attachments/assets/27759c8c-0e56-4b92-8173-ba031b994e5c) | | | |

</details>

<details>
<summary>View Screenshots (after fixes)</summary>

| Screenshot 1 | Screenshot 2 | Screenshot 3 | Screenshot 4 | Screenshot 5 | Screenshot 6 |
|--------------|--------------|--------------|--------------|--------------|--------------|
| ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 30](https://github.com/user-attachments/assets/71c0287d-d98a-4bfe-85dc-1e08da12ce43) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 26](https://github.com/user-attachments/assets/45eac165-f42a-4193-98cc-3ee85d963382) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 23](https://github.com/user-attachments/assets/c0e4432d-2d1d-487f-8965-3829c3257364) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 19](https://github.com/user-attachments/assets/ab81500c-66c1-4b90-b2b1-f7c490e0029b) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 16](https://github.com/user-attachments/assets/aafcad74-e851-49a8-9db0-df06f81f0b30) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 11](https://github.com/user-attachments/assets/29a2a6c8-e448-4c90-844d-29a248f9ab69) |
| ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 05 07](https://github.com/user-attachments/assets/54e505c4-c554-4000-aac1-61b97e034f55) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 04 39](https://github.com/user-attachments/assets/0e91dc29-199d-424e-b36a-6b0f7a317cd2) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 04 22](https://github.com/user-attachments/assets/e965b01d-0b03-4d01-a971-57c3bd550146) | ![Simulator Screenshot - iPhone SE (3rd generation) - 2024-12-11 at 00 04 15](https://github.com/user-attachments/assets/50ef03a6-bf1a-4db3-845b-e98373369574) |  |  |

</details>

Reviewed By: cipolleschi

Differential Revision: D67087492

Pulled By: javache

fbshipit-source-id: c9c64377d8c10d965bc5db7783aa80f099ce858a
2024-12-12 04:07:10 -08:00
Mateo Guzmán dffa57fa25 Exclude packages/react-native/ReactAndroid/build from lint checks (#48217)
Summary:
When running the linter locally, noise is generated from the `packages/react-native/ReactAndroid/build` folder. This folder does not need to be checked, as it is already excluded in the [.gitignore](https://github.com/facebook/react-native/blob/main/.gitignore#L33).

## Changelog:

[INTERNAL] - Exclude `packages/react-native/ReactAndroid/build` from lint checks

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

Test Plan:
```bash
yarn lint
```

Reviewed By: huntie

Differential Revision: D67134035

Pulled By: cortinico

fbshipit-source-id: f314c8601d6a3bf8ac6ebed67bdc392c6a6aeba8
2024-12-12 03:12:32 -08:00
Panos Vekris e5a526ff44 remove as_const option (on by default) in fbsource (#48227)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48227

This option is on by default in Flow v0.256

Reviewed By: gkz

Differential Revision: D67117062

fbshipit-source-id: 1595afe48178529ad43b33a215d84ff225cf9fa9
2024-12-12 02:01:35 -08:00
Andrew Coates 849407ae6f Fix no return static analysis error in SchedulerPriorityUtils.h (#47911)
Summary:
In react-native-windows our static analysis tools report an error for `timeoutForSchedulerPriority` due to cases where it may not always return a value.  This is an upstreaming of the patch we have to fix that error.

## Changelog:

[INTERNAL] [FIXED] - Fix no return static analysis error in SchedulerPriorityUtils.h

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

Test Plan: Building should be sufficient.

Reviewed By: christophpurrer

Differential Revision: D66992063

Pulled By: NickGerleman

fbshipit-source-id: 999fea328d0c66ad92314f537e41beff5856c285
2024-12-12 01:31:18 -08:00
Chi Tsai e34c1e9bd2 Add default implementation for Object.create(prototype) (#47946)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47946

Object creation with custom prototype can currently be done, but it is
unnecessarily convoluted. Users have to call into the global object to
get the `Object.create` function, then call it with the custom
prototype.

This diff adds a JSI API for Object.create(prototype) to make it easy
for users.

Changelog: [Internal]

Reviewed By: avp

Differential Revision: D66485209

fbshipit-source-id: 32018f847190ac16f695f011a78be0c45c4c4659
2024-12-12 01:14:00 -08:00
Chi Tsai 04f33ecd58 Add default implementation for Object.getPrototypeOf and Object.setPrototypeOf (#47996)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47996

Getting and setting an Object's prototype is convoluted. Users have to
call into the global object to get the method, then call it.

This diff adds a JSI API for Object.getPrototype and Object.setPrototype
to make it easy for users.

Changelog: [Internal]

Reviewed By: fbmal7

Differential Revision: D66562549

fbshipit-source-id: 85a2e49deb9d00500544de4cc5ab123c4717398e
2024-12-12 01:14:00 -08:00
Christoph Purrer 07d723cb72 Remove unused (Android) TextLayoutManager getter (#48208)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48208

[Changelog] [Internal] - Remove unused (Android) TextLayoutManager getter

This getter
```
void* getNativeTextLayoutManager() const;
```
is not used on Android and different in signature to the iOS one:
```
std::shared_ptr<void> getNativeTextLayoutManager() const;
```
Deleting it for now to make future code sharing between various platforms easier.

This change removes further the by now discouraged pattern of:
```
using SharedTextLayoutManager = std::shared_ptr<const TextLayoutManager>;
```
and changes callers to use
```
std::shared_ptr<const TextLayoutManager>;
```
directly.

Reviewed By: javache

Differential Revision: D67059514

fbshipit-source-id: b94dc7f664083c5c62c4ba7defca480549ca9dc1
2024-12-11 23:45:21 -08:00
hoxyq (Meta Employee) a80baac58e Remove comment syntax from ReactNativeTypes (#31457)
Summary:
# Summary

I'm working to get the main `react-native` package parsable by modern
Flow tooling (both `flow-bundler`, `flow-api-translator`).

This diff trivially removes some redundant Flow comment syntax in
`ReactNativeTypes.js`, which fixes parsing under these newer tools.

## How did you test this change?

Files were pasted into `react-native-github` under fbsource, where Flow
validates .

DiffTrain build for [92b62f500c3fca44a9dc9ead936ef3bf19481f02](https://github.com/facebook/react/commit/92b62f500c3fca44a9dc9ead936ef3bf19481f02)

Reviewed By: huntie

Differential Revision: D67100354

Pulled By: hoxyq

fbshipit-source-id: 575e4bd8ceefad15576273920a263ae89d027cad
2024-12-11 22:47:38 -08:00
Sam Zhou 7000b9b76c Add annotations and casts to fix future flow error (#48219)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48219

Changelog: [internal]

Reviewed By: panagosg7

Differential Revision: D67099060

fbshipit-source-id: fbb975d6194b071aa8f30dcd2b8f7f40130d31fb
2024-12-11 14:16:58 -08:00
Riccardo Cipolleschi 3ff9212ce4 Fix React-graphics podspec source_files (#48216)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48216

The `React-graphics` podspec is manually excluding some paths from the source_files but this approach is error prone. For esample changes that added new paths that must be excluded can create failures.

This change the podspec file to explicitly add only the files that iOS requires.

## Changelog:
[iOS][Changed] - Explicitly define the source files for React-graphics

## Facebook:
This change is necessary because we added the macos platform that was not included before and now we can't build RNTester from fbsource using the internal pipeline.

Reviewed By: hoxyq

Differential Revision: D67095677

fbshipit-source-id: 701d5938f6e141a313be62c8f930a089e1d6ee96
2024-12-11 08:48:31 -08:00
Hanno J. Gödecke 03d2186ace Construct RawValue directly from jsi::Value (#48047)
Summary:
### Motivation:

We are looking for a way to access the "raw" jsi value in our fabric view components, so that we can pass complex types like `HostObjects` or `jsi::Object` with `NativeState` attached to our components directly.
Currently the props are converted from `jsi::Object` to `folly::dynamic`, which prevents us from accessing these values directly.

### Changes

This PR is a implementation of the proposal discussed here:

- https://github.com/facebook/react-native/pull/44966#issuecomment-2503915245

These changes extend `RawValue` so that it can be directly constructed from `RawValue(Runtime*, jsi::Value&)` (not just from `folly::dynamic`).

`RawValue`s are created by the `RawPropParser.cpp`. By default it will use the `RawValue(folly::dynamic)` constructor, but we added a feature flag called `useRawPropsJsiValue`, which will sue the JSI constructor.
This enables to test this feature at runtime incrementally.

This change might also be tested on a component basis, by setting a flag in the component descriptor to enable JSI prop parsing for the RawPropParser, as outlined in this PR:

- https://github.com/hannojg/react-native/pull/2

## 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
-->

[General] [Added] - Add `useRawPropsJsiValue` feature flag to represent props internally as `jsi::Value`s instead of converting them to `folly::dynamic`

[General] [Added] - Added `RawValue(Runtime*, jsi::Value&)` constructor to make a `RawValue` from a `jsi::Value`.

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

Test Plan: Enable the `useRawPropsJsiValue` feature flag and test the rn-tester app on android and iOS. Make sure you get no new errors / warnings in the native console.

Reviewed By: NickGerleman

Differential Revision: D66877093

Pulled By: javache

fbshipit-source-id: 7342e5f86d2492ad63a9ccf5508f04e7eb252def
2024-12-11 08:38:12 -08:00
Juliusz Wajgelt 7d0338cb0b fix incorrect mmap file offset in JSBigFileString (#48198)
Summary:
`JSBigFileString` incorrectly passes the file offset to `mmap`, causing errors when `offset` is non-zero.

## Changelog:
[GENERAL] [FIXED] - `JSBigFileString` fails for non-zero offset arguments

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

Test Plan: - verify the new unit test passes

Reviewed By: cipolleschi

Differential Revision: D67086826

Pulled By: javache

fbshipit-source-id: 0991bb34a710b85ff0263dc553efb85ba62b4cd8
2024-12-11 08:24:50 -08:00
Mitya Kononchuk 477489ce84 Fix running fantom tests via buck-test (#48215)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48215

Changelog: [internal]

Reviewed By: rubennorte

Differential Revision: D66976980

fbshipit-source-id: d553c2f702d6928fb0ad88fcc2c6f04160a65d02
2024-12-11 07:07:10 -08:00
Ben Handanyan 462fae4a29 Use configuration type when adding ndebug flag to pods in release (#48193)
Summary:
While I was [working on fixing the iOS debugger logic](https://github.com/facebook/react-native/pull/48174) based on configuration name regex match, I wanted to know if other logic was also based on configuration names. I think I found and fixed the only other configuration name-based logic in the repo in this PR.

## Changelog:

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

Pick one each for the category and type tags:

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

[IOS] [CHANGED] - Use configuration type when adding ndebug flag to pods in release

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

Test Plan:
In a fresh react-native project, I added to the Podfile:
```ruby
    installer.aggregate_targets.each do |aggregate_target|
      aggregate_target.xcconfigs.each do |config_name, config_file|
          is_release = aggregate_target.user_build_configurations[config_name] == :release
          puts "aggregate_targets #{config_name} is_release: #{is_release}"
        end
    end

    installer.target_installation_results.pod_target_installation_results.each do |pod_name, target_installation_result|
      target_installation_result.native_target.build_configurations.each do |config|
          is_release = config.type == :release
          puts "target_installation_results #{config.name} is_release: #{is_release}"
      end
    end
```

to confirm my logic. It output the following:
```
aggregate_targets Release is_release: true
aggregate_targets Local is_release: false
...
target_installation_results Local is_release: false
target_installation_results Release is_release: true
...
```

I also updated the applicable tests I could find for this logic.

Reviewed By: cortinico

Differential Revision: D67025325

Pulled By: cipolleschi

fbshipit-source-id: 45d68ee86e3255d843275a72916883c8c4bbc13d
2024-12-11 04:37:48 -08:00
Christoph Purrer 7d771de8a7 Share common ShadowNode functionality in BaseTextInputShadowNode for iOS (#48164)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48164

[Changelog] [Internal] - Share common ShadowNode functionality in BaseTextInputShadowNode for iOS

The current Android and iOS implementations have quite some overlapping functionality. Not sharing common logic makes it also harder to reuse this [functionality] for out of tree platforms.

This change moves the current iOS implementation into a shared location.
The next change allows to reuse it for Android.

Reviewed By: NickGerleman

Differential Revision: D66901676

fbshipit-source-id: a870155633875377d881fbd9f41fafb305672949
2024-12-10 18:05:42 -08:00
Pieter De Baets 93c6be496e Configure __RCTProfileIsProfiling for perfetto builds (#48201)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48201

For any build for which we enable the perfetto build flag, we should probably enable the `__RCTProfileIsProfiling` so we get systrace markers.

Changelog: [Internal]

Reviewed By: bgirard

Differential Revision: D67031171

fbshipit-source-id: 5e7f56f911acacd3156778bd9151202fc809e291
2024-12-10 16:53:05 -08:00
Mateo Guzmán 9ecf290d27 Paper: TextInput maxLength is not working in old arch (#48126)
Summary:
Fixes https://github.com/facebook/react-native/issues/47563

It seems like a regression from https://github.com/facebook/react-native/issues/45401, where it was aimed to fix `onChangeText` being called multiple times when changing the text programmatically in an input with the `multiline` prop set as true.

This PR reverts that partially, as the `maxLength` check is not being evaluated correctly before setting the text. Not reverting it completely as when removing the second part of the fix, the `onChangeText` gets called multiple times again.

## Changelog:

[IOS] [FIXED] - Fixing TextInput `maxLength` not working in old arch

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

Test Plan:
The issue could be reproduced in the rn-tester. See my videos with the before and after the fix.

<details>
<summary>Before:</summary>

https://github.com/user-attachments/assets/86fd67eb-fc14-469a-a5f8-8e83b49f857c

</details>

<details>
<summary>After:</summary>

https://github.com/user-attachments/assets/368383b1-c1bd-4e0b-ac44-c78022462fa0

</details>

Reviewed By: cortinico

Differential Revision: D67025182

Pulled By: cipolleschi

fbshipit-source-id: 720c400eef362618106ae434aef421c7529214fe
2024-12-10 11:57:48 -08:00
zhongwuzw d20897f6d4 Fabric: Fixes transform when there are multiple values that contain a matrix (#47477)
Summary:
Fixes https://github.com/facebook/react-native/issues/47467 .

## Changelog:

[IOS] [FIXED] - Fabric: Fixes transform when there are multiple values that contain a matrix

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

Test Plan: Repro in Fixes https://github.com/facebook/react-native/issues/47467

Reviewed By: javache

Differential Revision: D65594337

Pulled By: cipolleschi

fbshipit-source-id: 50f255e753e2f233415099c3fbdd0e43b0afefc0
2024-12-10 10:29:29 -08:00
Mateo Guzmán 9a21b99918 Making RCTNetworking js exports consistent (#48166)
Summary:
Fixes https://github.com/facebook/react-native/issues/39260

Right now, there is a small issue when you try debugging the Networking library methods as it seems like they are empty in Android. This is not an actual functional issue as everything in code works fine, but rather an inconsistency in how the iOS and Android methods are being exported. In iOS it was exported as an object, in Android it was a class.

## Changelog:

[INTERNAL] - Making `RCTNetworking` js exports consistent

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

Test Plan:
I've checked that `XMLHttpRequest` is still working as expected, as this is used mostly there.

And below there are screenshots of how the module methods are logged after the refactor. Which addresses what was reported in the linked issue.

```js
import {Networking} from 'react-native';
import AndroidNetworking from 'react-native/Libraries/Network/RCTNetworking.android.js';
import IOSNetworking from 'react-native/Libraries/Network/RCTNetworking.ios.js';

console.log({Networking, AndroidNetworking, IOSNetworking});
```

Before | After
-- | --
<img width="1196" alt="image" src="https://github.com/user-attachments/assets/b7ab1dcd-9dd1-4ed9-ade5-d90251a77d5e"> | <img width="1196" alt="image" src="https://github.com/user-attachments/assets/5ae17c6a-b068-462a-b228-576dcf08ef12">

Reviewed By: fabriziocucci

Differential Revision: D67022711

Pulled By: javache

fbshipit-source-id: 81f9988295fb3f559a795077f09ee0f14827dc86
2024-12-10 10:17:42 -08:00
Rubén Norte 6ba8b65e9f Remove legacy ReactNativeConfig abstraction (#47247)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47247

Changelog: [internal]

Bye bye `ReactNativeConfig` 👋.

All existing usages of the API have been cleaned up or migrated to `ReactNativeFeatureFlags`, so this is no longer needed.

Reviewed By: GijsWeterings

Differential Revision: D65062306

fbshipit-source-id: 76afcd48ad72023b6dc2a90955ae2f03a1164cca
2024-12-10 09:34:27 -08:00
Rubén Norte c24f963330 Make ReactNativeConfig an empty interface (#47246)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47246

Changelog: [internal]

Just to use CI to verify there are no more existing usages of the API before cleaning it up.

Reviewed By: GijsWeterings

Differential Revision: D65062302

fbshipit-source-id: e1b71d39ef1fba23cc68e36fe0a0b57cfc2a614e
2024-12-10 09:34:27 -08:00
Pieter De Baets 746d584a23 Fix animation.stop not being flushed (#48199)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48199

`animatedShouldUseSingleOp` relies on a queue always being active, or for a call to `flushQueue` later down the line. If these are missing, a call will be queued up but only executed whenever the next animation flush happens.

Changelog: [General][Fixed] Animation.stop() executes when `animatedShouldUseSingleOp` is enabled.

Reviewed By: yungsters

Differential Revision: D67025831

fbshipit-source-id: 66a7f50d833b7bbaf9f16dd04d24b90c7a699fa0
2024-12-10 09:24:05 -08:00
Riccardo Cipolleschi 8ab524312a Skip hidden folders when looking for third party components (#48182)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48182

Maintainers from SVG reached out because of an edge case they inencountered when generating the ComponentProvider. In their setup, they had a `.git` folder in the repo and the algorithm was spending a lot of time crawling the git folder.

In general, we should avoid crawling hidden folders.

This change fix that.

## Changelog:
[General][Fixed] - Skip hidden folders when looking for third party components.

Reviewed By: javache

Differential Revision: D66959345

fbshipit-source-id: 992a79f3cff22cd6a459e0272c8140bc329888da
2024-12-10 02:32:10 -08:00
Blake Friedman 538fa01fb3 Add Changelog for 0.77.0-rc.2 (#48192)
Summary:
Add Changelog for 0.77.0-rc.2

Changelog: [Internal] - Add Changelog for 0.77.0-rc.2

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

Reviewed By: christophpurrer

Differential Revision: D66998575

Pulled By: blakef

fbshipit-source-id: 4dbfa3cae00b69ed8dd199a2b36cbd095ef4cbc2
2024-12-10 01:57:59 -08:00
Alex Taylor (alta) 8aeba2a3b8 Deploy 0.256.0 to xplat (#48190)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48190

Changelog: [Internal]

Reviewed By: SamChou19815

Differential Revision: D66979174

fbshipit-source-id: d94e8c42edd225fe367aac75e55d2c1d054acc07
2024-12-10 01:47:05 -08:00
Blake Friedman 3b079f38f4 Add Changelog for 0.76.5 (#48191)
Summary:
Add Changelog for 0.76.5

Changelog: [Internal] - Add Changelog for 0.76.5

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

Reviewed By: christophpurrer

Differential Revision: D66993996

Pulled By: blakef

fbshipit-source-id: 4d69bcea64822aa108c2bac24a3ab399aacd5419
2024-12-09 23:31:40 -08:00
Andrew Datsenko a298ccabe2 Add snapshot update / delete capability (#48096)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48096

Changelog: [Internal]

Managing snapshot state internally by adding / updating snapshot data.

Reviewed By: christophpurrer

Differential Revision: D66707175

fbshipit-source-id: 0366d834eafa0ca702f03de0210392181fd90a58
2024-12-09 19:09:57 -08:00
Andrew Datsenko a8a136f3ef Add .toMatchSnapshot() (#48029)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48029

Changelog: [Internal]

Adding snapshot support for rendered output only for now.
This will only work if snapshot is created beforehand by hand.

# Next steps
* Create snapshot when no prior snapshot is available
* Pass and update if instructed

Reviewed By: christophpurrer

Differential Revision: D66601387

fbshipit-source-id: fe528cded43c5ba36d314bd9af8e3fb84b98ac3e
2024-12-09 19:09:57 -08:00
Andrew Datsenko a4a2c2867a Add jest-snapshot (#48095)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48095

Changelog: [Internal]
Add `jest-snapshot` `v29.7.0`

Reviewed By: christophpurrer

Differential Revision: D66714069

fbshipit-source-id: 783584519e95b337d36c4a00610bcd970a041d4d
2024-12-09 19:09:57 -08:00
Jorge Cabiedes Acosta 6d235853fb Fix background getting clipped when border-radius is set (#47939)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47939

Before we were clipping Android background drawable to the padding box. This is not how its done on web.

The background should remain under the border so that if the border is traslucent you can see the background underneath

Reviewed By: NickGerleman

Differential Revision: D66463305

fbshipit-source-id: 427acea760b2748a07cc28bbd362aaaae0811093
2024-12-09 18:35:45 -08:00
Christoph Purrer 0b866aa40d Share common (Base)TextInputState properties (#48133)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48133

[Changelog] [Internal] -  Share common (Base)TextInputState properties

This change allows to share common TextInput State properties between various platforms.

Reviewed By: rshest

Differential Revision: D66855831

fbshipit-source-id: d0f85c419b82445ac84bfcc606f1bf752f5dba73
2024-12-09 15:02:58 -08:00
heoblitz 331d99a941 Update YGNodeStyleGetGap to return YGValue (#47973)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47973

Gap can be styled using both `points` and `percentages`, but YGNodeStyleGetGap currently returns a float value.

To maintain alignment with the `padding` and `margin` functionalities and allow it to be handled in bridging code, this function has been updated to return YGValue.

X-link: https://github.com/facebook/yoga/pull/1753

Reviewed By: joevilches

Differential Revision: D66513236

Pulled By: NickGerleman

fbshipit-source-id: b7110855c037f20780f031f22a945bde4446687d
2024-12-09 13:38:05 -08:00
Pieter De Baets 4ec4a85b1b Unbreak OSS Android CI (#48186)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48186

Broken by https://github.com/facebook/react-native/commit/37c532a063c6054ea974612a40551f7c1399c147 since we use different OkHttp versions internally and in open-source.

Changelog: [Internal]

Reviewed By: tdn120

Differential Revision: D66968229

fbshipit-source-id: a110cec0f9bd55e6ae5c90b766e8e6220703e6c3
2024-12-09 13:11:18 -08:00
Christoph Purrer 7ea5deb802 Remove unused defaultThemePaddingStart|End|Top|Bottom from AndroidTextInputState (#48161)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48161

[Changelog] [Internal] -  Remove unused defaultThemePaddingStart|End|Top|Bottom from AndroidTextInputState

This data is set, but never read

Reviewed By: javache

Differential Revision: D66904641

fbshipit-source-id: 4db1cd49e9ec63b62f75070b478d2006ea101f8c
2024-12-09 11:22:57 -08:00
Nick Lefever aef13d1f43 Enable shadow node reference updates by default (#48180)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48180

Changelog: [Internal]

Releasing runtime shadow node reference updates, enabling it by default now that the fix for RN Windows landed.

Reviewed By: javache

Differential Revision: D66959896

fbshipit-source-id: dcf1c4a7257fe60ae0faffe1952fc2c80effe406
2024-12-09 10:52:21 -08:00
Pieter De Baets c31b42aaa2 Fix flow-type of selectionColor (#48184)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48184

Changelog: [Internal]

Reviewed By: SamChou19815

Differential Revision: D66962089

fbshipit-source-id: cd45784d830a12a620800290235a5a1cad3097f3
2024-12-09 10:20:12 -08:00
Jakub Piasecki 47822e9048 Fix adjustsFontSizeToFit for strings with a single character (#47082)
Summary:
Fixes https://github.com/facebook/react-native/issues/47045

On Android `adjustsFontSizeToFit` relies on two metrics:
- Text with line breaks results in more lines than `maximumNumberOfLines`
- The overall height of the text is larger than the available height

None of these two was fulfilled when a single-character string had a higher width than the available one (a single character will not be broken into multiple lines). This PR adds exactly that as a third option to trigger the scaling algorithm - a single-character string that has a higher width than the available one.

On iOS `adjustsFontSizeToFit` relies on `truncatedGlyphRangeInLineFragmentForGlyphAtIndex` which seems to be returning `NSNotFound` when a single-character sting gets truncated. Similarly to Android, this PR adds an additional check to make sure that single-character strings actually fit inside the container.

## Changelog:

[GENERAL] [FIXED] - Fixed `adjustsFontSizeToFit` not working for text with a single character

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

Test Plan:
Tested on the code from the issue:

|Android (old arch)|Android (new arch)|iOS (old arch)|iOS (new arch)|
|-|-|-|-|
|<img width="406" alt="android_old" src="https://github.com/user-attachments/assets/91b1af41-4ef7-46cc-bb04-374f860d93ac">|<img width="406" alt="android_new" src="https://github.com/user-attachments/assets/90e3cde1-e6c0-4b25-8325-c62a37773002">|<img width="546" alt="ios_old" src="https://github.com/user-attachments/assets/902b9c10-84e0-4372-bcc8-07cd1ef006f6">|<img width="546" alt="ios_new" src="https://github.com/user-attachments/assets/f4df4f0e-7649-47f3-9c81-e38f8665d9a2">|

Reviewed By: javache

Differential Revision: D64664351

Pulled By: NickGerleman

fbshipit-source-id: b68f318a0fbd5ebed947a70d1e3fb0515b5fb409
2024-12-09 10:12:10 -08:00
Ben Handanyan eda4f185b3 Enable hermes debugger by configuration type instead of configuration name (#48174)
Summary:
Fixes an [issue](https://github.com/facebook/react-native/issues/48168) where only iOS configurations with "Debug" in the name are configured to use the hermes debugger.

## Changelog:

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

Pick one each for the category and type tags:

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

[IOS] [FIXED] - Enable hermes debugger by configuration type instead of configuration name

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

Test Plan:
Added new test scenarios that all pass:
```
ruby -Itest packages/react-native/scripts/cocoapods/__tests__/utils-test.rb
Loaded suite packages/react-native/scripts/cocoapods/__tests__/utils-test
Started
Finished in 0.336047 seconds.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
56 tests, 149 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
166.64 tests/s, 443.39 assertions/s
```

In a personal project with the following configurations:
```
project 'ReactNativeProject', {
    'Local' => :debug,
    'Development' => :release,
    'Staging' => :release,
    'Production' => :release,
  }
```
I added the following to my Podfile:
```
installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
        puts "#{config.name} is debug? #{config.type == :debug}"
    end
end
```
To confirm that my logic is correct:
```
Local is debug? true
Development is debug? false
Staging is debug? false
Production is debug? false
```

Reviewed By: robhogan

Differential Revision: D66962860

Pulled By: cipolleschi

fbshipit-source-id: 7bd920e123c9064c8a1b5d45df546ff5d2a7d8be
2024-12-09 08:51:00 -08:00
Pieter De Baets 0916d530f0 Allow CookieJar to be disabled in NetworkingModule (#48113)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48113

Processing cookies can have a non-trivial impact on startup-time. It requires reading OkHttp's `PublicSuffixDatabase` but also allocating various WebKit components. Instead handle the cookiejar being set to non-CookieJarContainer instances gracefully, which allows a custom client builder to set `CookieJar.NO_COOKIES`.

Changelog: [Internal]

Reviewed By: cortinico

Differential Revision: D66787514

fbshipit-source-id: bf790691496f674ec743ba4791552b12e06eda29
2024-12-09 08:37:32 -08:00
Mateo Guzmán 37c532a063 test(image): [android] react okhttp network fetcher cache control tests (#47953)
Summary:
This is a follow up for the new cache control options for the Android Image component introduced in https://github.com/facebook/react-native/issues/47182, https://github.com/facebook/react-native/issues/47348 & https://github.com/facebook/react-native/issues/47426. And to make sure the cache control header works as expected and avoid missing the issue fixed in https://github.com/facebook/react-native/issues/47922, this PR introduces test cases to make sure this is getting applied as expected in the `ReactOkHttpNetworkFetcher`.

## Changelog:

[INTERNAL] [ADDED] - `ReactOkHttpNetworkFetcher` cache control tests

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

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

Reviewed By: rshest

Differential Revision: D66498305

Pulled By: javache

fbshipit-source-id: 7a9a0cc596e49964943e59189614743ca8a472a1
2024-12-09 07:36:46 -08:00
Rubén Norte 3cc67fed36 Add Fantom mode for development with Hermes bytecode (#48178)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48178

Changelog: [internal]

Adds a new mode for Fantom to run tests with dev-mode bytecode. Right now the modes were only dev (development with source code) or opt (optimized bytecode).

Reviewed By: rshest

Differential Revision: D66888986

fbshipit-source-id: 34b2566a65d138790e16f8fb5787fd9c2bcde536
2024-12-09 05:43:27 -08:00
Rubén Norte 4d07fb7662 Use enum for Fantom modes (#48179)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48179

Changelog: [internal]

Migrating this type to an enum, which is safer, because it prevents errors like:

```
// when it's actually 'dev'
if (mode === 'development') {

}
```

Reviewed By: rshest

Differential Revision: D66888985

fbshipit-source-id: 4f3f91fad6ca5256baa2123425b2bad11fe036f9
2024-12-09 05:43:27 -08:00
Rubén Norte 07a7b63fdd Only prewarm in CI (#48151)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48151

Changelog: [internal]

We have global setup step in Fantom to prewarm caches to properly attribute test running time, but this isn't necessary when running tests locally. Attribution isn't as important there. This disables the prewarming step so we can run individual tests as fast as we can.

Reviewed By: sammy-SC

Differential Revision: D66877990

fbshipit-source-id: 1f33c19a3c537c1c0e499fd7a6c405450cb9f86d
2024-12-09 05:43:27 -08:00
Rubén Norte 82abba9936 Implement warmup for optimized mode (#48150)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48150

Changelog: [internal]

We're starting to have some Fantom tests that run in optimized mode, but we're not currently prewarming for that case. This adds that capability to do proper attribution of run time for tests.

Reviewed By: javache

Differential Revision: D66877991

fbshipit-source-id: dccb80cd6a4f664de7df0661456bad78d960826d
2024-12-09 05:43:27 -08:00
Rubén Norte 47589f53b1 Add tests for Fantom modes (#48123)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48123

Changelog: [internal]

This verifies that the modes specified in the pragmas are applied correctly.

Reviewed By: andrewdacenko

Differential Revision: D66822377

fbshipit-source-id: 420f21f171c5d356ab91b49f7a33345386f6f0c0
2024-12-09 05:43:27 -08:00
Rubén Norte d05214665c Delete tests migrated to Fantom and unnecessary mocks for FabricUIManager, DOM, etc. (2nd attempt) (#48117)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48117

Changelog: [internal]

Re-land https://github.com/facebook/react-native/pull/48087 with some CI fixes.

Reviewed By: rshest

Differential Revision: D66820310

fbshipit-source-id: 1df4559c1daf5ec0085b299d702ce36deaa681b5
2024-12-09 05:05:07 -08:00
Pieter De Baets 351b1bae95 Use OkHttp3 for NetworkingModuleTest (#48012)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48012

Match the OkHttp version we use for NetworkingModule and FrescoModule, to unblock pulling in D66498305

Changelog: [Internal]

Reviewed By: cortinico

Differential Revision: D66595222

fbshipit-source-id: 1c29b061866be5d8bcc87aaa0c8a1de846198e4e
2024-12-09 05:00:41 -08:00
Nicola Corti 8babc21b79 Convert com.facebook.react.jstasks to Kotlin (#48147)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48147

This converts all the classes inside `com.facebook.react.jstasks` to Kotlin

Changelog:
[Internal] [Changed] -

Reviewed By: javache

Differential Revision: D66875442

fbshipit-source-id: 0a9d485e3902626a04db5e7a1a0ccad32b2bc44c
2024-12-09 04:17:14 -08:00
Blake Friedman 4165884b70 Add Changelog for 0.76.4 (#48163)
Summary:
Add Changelog for 0.76.4

Changelog: [Internal] - Add Changelog for 0.76.4

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

Reviewed By: cipolleschi

Differential Revision: D66908649

Pulled By: blakef

fbshipit-source-id: cb64fcc5dbc6a1f758d99f99c451f24374fd4768
2024-12-07 09:46:51 -08:00
Christoph Purrer e9f92fad0a Remove unused code in AndroidTextInputShadowNode.h|cpp (#48136)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48136

[Changelog] [Internal] -  Remove unused code in AndroidTextInputShadowNode.h|cpp

A bit of code clean-up to simplify a planned refactoring of this class

Reviewed By: rshest

Differential Revision: D66862820

fbshipit-source-id: 88114d8711b572f105d804cdddc6c087c94e3f49
2024-12-06 20:38:43 -08:00
Dmitry Rykun 6200a4d330 Annotate the experimental image prefetching API as @UnstableReactNativeAPI (#48120)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48120

This diff annotates the experimental image prefetching API as `UnstableReactNativeAPI` instead of `Deprecated`.

Changelog: [Internal]

Reviewed By: cortinico, philIip

Differential Revision: D66822045

fbshipit-source-id: a95ef3112a621a0735d4dcda0aed6078be7d7a38
2024-12-06 17:49:40 -08:00
Jorge Cabiedes Acosta 4102aa4a6b Revise iOS's implementation of ensureNoOverlap for borders (#48094)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48094

There were a few math inaccuracies in the algorithm for overlapping radii. After fixing there were also minor pixel differences on the unit tests but this is the most correct implementation.

Also, improved the algorithm's verbiage since stuff like "EdgeInset" is not really related and is misleading to what the algorithm is actually doing. (Edge Insets play no part in this)

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66728227

fbshipit-source-id: 56a6d59504e784fc245ed6fe306402a15cfd9611
2024-12-06 16:05:44 -08:00
Joe Vilches 0b40cb8b7f Use crossAxisOwnerSize instead of ownerHeight in cross axis bound call (#48080)
Summary:
X-link: https://github.com/facebook/yoga/pull/1763

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

Small bug that I noticed while doing intrinsic sizing. We have the ownerHeight as the axis size despite bounding the length of the cross axis. This should therefore be the crossAxisOwnerSize, which might be the width in some cases

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66736539

fbshipit-source-id: 528fc438b3327cd6f7890ea0ba408e4ce7b0f02c
2024-12-06 15:34:06 -08:00
Pieter De Baets 3c5019a376 Remove enableFabricRendererExclusively feature-flag (#48157)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48157

Changelog: [Internal]

Reviewed By: jehartzog

Differential Revision: D66787414

fbshipit-source-id: f27cc551fd7ceabf9fc656db2810476815fce1bf
2024-12-06 14:49:10 -08:00
Zeya Peng 4238299bf3 Allow setting debugID on all types of AnimatedNode and Animation (#48129)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48129

Continuation of https://github.com/facebook/react-native/pull/48106

* Every AnimatedNode subclasses now have an optional `config` arg as last arg in constructor. `Animation` base constructor already takes in config with debugID, since last PR.
* thread down debugID value to all the native configs

Changelog: [Internal] Allow setting debugID on all types of AnimatedNode and Animation

Reviewed By: yungsters

Differential Revision: D66834935

fbshipit-source-id: 18e5cbc3f701114ef945a237cb5944ef5eb6408e
2024-12-06 14:47:30 -08:00
Christoph Purrer d47ff26f37 Allow to provide a custom TextLayoutManager for cxx platform (#48127)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48127

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

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

Reviewed By: zeyap

Differential Revision: D66802434

fbshipit-source-id: a64e28d357bf601c7234b43f86538f49e62c8435
2024-12-06 13:38:58 -08:00
Blake Friedman 05b4146270 ci: verify JS build artifacts aren't committed (#48091)
Summary:
Building our JS packages dirties the repo. This makes sure we don't
accidentally commit these to the repo, as it'll break Flow tests with an
obscure error message.

Changelog: [Internal]

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

Test Plan:
Ran this locally, the GH lint action should run on this PR.
I'll intentionally add a build artifact to validate.
| A | B | C |
| - | - | - |
| ![CleanShot 2024-12-04 at 14 11 16@2x](https://github.com/user-attachments/assets/11c05c32-12c8-4d85-9a28-b3bdffb42ea0) | ![CleanShot 2024-12-04 at 14 11 22@2x](https://github.com/user-attachments/assets/b4c4f1dc-2cb9-4138-8931-13e71c015a6d) | ![CleanShot 2024-12-04 at 14 19 31@2x](https://github.com/user-attachments/assets/953bf783-57ba-4832-bbd8-b36e23a2ccb4) |

Reviewed By: cipolleschi

Differential Revision: D66760144

Pulled By: blakef

fbshipit-source-id: 81f20fa0a83d5f17b5773d1608664b683ba74409
2024-12-06 13:21:23 -08:00
Kudo Chien e42a3a6b84 Migrate jsc-android to mavenCentral (#47972)
Summary:
Since people mostly use Hermes, it doesn't make sense to download jsc-android from npm even when jsc is not used. This PR migrates the jsc-android to [mavenCentral](https://repo1.maven.org/maven2/io/github/react-native-community/jsc-android/2026004.0.0/). The new jsc-android supports Android 16KB memory page sizes and packaged by prefab.
Relevant PRs:
  - https://github.com/react-native-community/jsc-android-buildscripts/pull/184
  - https://github.com/react-native-community/jsc-android-buildscripts/pull/185

## Changelog:

[ANDROID] [CHANGED] - Migrate jsc-android to mavenCentral

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

Test Plan: CI passed

Reviewed By: cipolleschi

Differential Revision: D66772407

Pulled By: cortinico

fbshipit-source-id: e34d2d138996e394763ef67d7aad65bb3e7b13dc
2024-12-06 12:50:34 -08:00
Dmitry Rykun c9ac94a000 Rename shouldNotify to shouldNotifyLoadEvents (#48100)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48100

This diff renames `shouldNotify` to `shouldNotifyLoadEvents` as it is named in the spec.
Changelog: [Internal]

Reviewed By: javache

Differential Revision: D66769660

fbshipit-source-id: 64282c08ab82101d51dedb583e0c34476ed90eeb
2024-12-06 12:06:33 -08:00
Mateo Guzmán f5506dc1d0 Migrate HeadlessJsTaskEventListener to Kotlin (#48103)
Summary:
Migrate `HeadlessJsTaskEventListener` to Kotlin

## Changelog:

[INTERNAL] - Migrate `HeadlessJsTaskEventListener` to Kotlin

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

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

Reviewed By: javache

Differential Revision: D66814483

Pulled By: cortinico

fbshipit-source-id: 5aef4ce020f97164845e3e0d53a107c7e407a6aa
2024-12-06 12:06:20 -08:00
Nicola Corti 4560fc0497 Fix crash on HeadlessJsTaskService on old architecture (#48124)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48124

Fixes https://github.com/facebook/react-native/issues/47592

The logic in HeadlessJsTaskService is broken. We should not check whether `getReactContext` is null or not.
Instead we should use the `enableBridgelessArchitecture` feature flag to understand if New Architecture was enabled or not.

The problem we were having is that `HeadlessJsTaskService` was attempting to load the New Architecture even if the user would have it turned off. The Service would then die attempting to load `libappmodules.so` which was correctly missing.

Changelog:
[Android] [Fixed] - Fix crash on HeadlessJsTaskService on old architecture

Reviewed By: javache

Differential Revision: D66826271

fbshipit-source-id: 2b8418e0b01b65014cdbfd0ec2f843420a15f9db
2024-12-06 12:05:58 -08:00
Nicola Corti ddfa2120ba Cleanup NoRetryPolicy unnecessary visibility. (#48146)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48146

The NoRetryPolicy class is `internal`. Having those `public` modifiers on methods has no effect
and can be safely removed.

Changelog:
[Internal] [Changed] -

Reviewed By: fabriziocucci

Differential Revision: D66875443

fbshipit-source-id: 64c63c7000617cf94c36ce3d25927d3a270ac370
2024-12-06 12:05:53 -08:00
Tim Yung 8793b7d89b RN: Backout "Scheduling Animated End Callbacks in Microtask" (#48132)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48132

Backs out D63573322 and D65645981, reverting the change that makes callbacks passed to `animation.start(<callback>)` scheduled for execution in a microtask.

This is being reverted becuase the latency introduced by the current macro and pending micro tasks can introduce visible latency artifacts that diminish the fidelity of animations.

Changelog:
[General][Changed] - Reverts #47503. (~~Callbacks passed to `animation.start(<callback>)` will be scheduled for execution in a microtask. Previously, there were certain scenarios in which the callback could be synchronously executed by `start`.~~)

Reviewed By: javache

Differential Revision: D66852804

fbshipit-source-id: 08434b9876813fe9e8b189b6b467198933843bf0
2024-12-06 12:05:46 -08:00
Thomas Nardone f15fe4b8a1 Convert WebSocketModule to Kotlin (#47491)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47491

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D65502519

fbshipit-source-id: fa75416a27f809701837e8e1b96d7fe3a1ea040a
2024-12-06 09:22:30 -08:00
Kræn Hansen acecf99c38 Update build-hermes-xcode.sh to fail faster (#47894)
Summary:
While debugging an error building hermes from source in a React Native app, I kept getting this weird error:

> Building for 'iOS-simulator', but linking in dylib ({redacted}/ios/Pods/hermes-engine/destroot/Library/Frameworks/ios/hermes.framework/hermes) built for 'macOS'

The root cause was a call to `cmake` failing, but `/sdks/hermes-engine/utils/build-hermes-xcode.sh` didn't exit on the error and instead continued building the hermes.framework from the dummy frameworks created by `./sdks/hermes-engine/utils/create-dummy-hermes-xcframework.sh`.

I suggest fixing this by introducing a `set -e` call similar to that used in `build-hermesc-xcode.sh`:
https://github.com/facebook/react-native/blob/2f523f0acf3b589bf962d5d20d2c04e453baf1da/packages/react-native/sdks/hermes-engine/utils/build-hermesc-xcode.sh#L7

## Changelog:

[Internal] - Ensure building hermes from source exits early on failures

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

Test Plan:
I followed https://github.com/facebook/hermes/blob/main/doc/ReactNativeIntegration.md getting to a state of building a React Native app with Hermes built from source. I then introduced an error in `hermes/API/hermes/hermes.cpp` (I simply typed `asd` in the top of the file) and built the app from Xcode:

![Screenshot 2024-11-22 at 15 13 04](https://github.com/user-attachments/assets/c8c4a1e2-3bf1-4d08-b9f9-583dba3df159)

If you scroll up, you can see the failed cmake build, but it just continues trying to build the framework, effectively hiding the error:

![Screenshot 2024-11-22 at 15 14 19](https://github.com/user-attachments/assets/3accf7b1-1667-42fc-b9c0-01fd9b1c2d8f)

When applying this patch and cleaning the build folder, the error is more prominent and actionable:

![Screenshot 2024-11-22 at 15 18 27](https://github.com/user-attachments/assets/f01afd6c-c1bb-4e87-9d28-dcdea41feb3e)

Reviewed By: andrewdacenko

Differential Revision: D66494603

Pulled By: dmytrorykun

fbshipit-source-id: dbeeba17b4cc1101001c9628914135bea6006d4a
2024-12-06 09:08:58 -08:00
Blake Friedman ae775aff5d Danger shouldn't warn for package.json changes (#48148)
Summary:
Someone always has to merge in from Meta, so this is just noise.
Refactored some of this older code.

Changelog: [Internal]

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

Test Plan:
This PR, but this doesn't build a lot of confidence:

 {F1973526183}

Reviewed By: rubennorte

Differential Revision: D66876722

Pulled By: blakef

fbshipit-source-id: 52e1f15577f8f057ceee9427af65df43f152bffa
2024-12-06 08:48:55 -08:00
Rubén Norte c54ba09d2e Add package.json script to run Fantom tests (#48144)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48144

Changelog: [internal]

Adds package.json script to run Fantom tests:

```
yarn fantom
```

NOTE: At the moment, this only works on Meta's infra. We're working on making this available in OSS/Github CI.

Reviewed By: javache

Differential Revision: D66874962

fbshipit-source-id: d9746428b618a31ce0bf96c3233828cdba501dd6
2024-12-06 05:24:31 -08:00
Rubén Norte 81fbd18410 Move remaining Fantom files to react-native-fantom (#48143)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48143

Changelog: [internal]

Just a small cleanup to move `jest/integration/*` to `packages/react-native-fantom`, so everything related to Fantom (config, runner, runtime, etc.) is in the same directory.

Reviewed By: javache

Differential Revision: D66874763

fbshipit-source-id: 8b87d7320c7704f7ce6cd58761508193784f5ce2
2024-12-06 05:24:31 -08:00
Rubén Norte 923b194e47 Rename ReactNativeTester as just Fantom (#48142)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48142

Changelog: [internal]

This is the default export from the `react-native/fantom` package and it makes sense to be called that way. Also, this is similar to the `jest` global.

Reviewed By: javache

Differential Revision: D66874225

fbshipit-source-id: 8b43a637ebb42b5b1acb9ea5a6dbedd4c1a4f9e0
2024-12-06 05:24:31 -08:00
Rubén Norte 3fafc9f9cf Rename FantomRenderedOutput as getFantomRenderedOutput to follow convention (#48141)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48141

Changelog: [internal]

This just aligns the file name with the common convention to name modules with the same name as their default export (if any).

Reviewed By: javache

Differential Revision: D66874227

fbshipit-source-id: 2a619b434c26a29f1774cba1c32ba711b1a7af46
2024-12-06 05:24:31 -08:00
Rubén Norte ba8d184b77 Move render output tests to ReactNativeTester (#48140)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48140

Changelog: [internal]

These tests test an API that's part of `ReactNativeTester` (will be renamed as `Fantom`) so it makes sense that they're in the same test file as the tests for the rest of the API.

Reviewed By: javache

Differential Revision: D66874226

fbshipit-source-id: f17e14c83cb5ca95ac619c5398c49ad84a27cfa5
2024-12-06 05:24:31 -08:00
Rubén Norte b253b0fe94 Create @react-native/fantom package (#48125)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48125

Changelog: [internal]

This just moves the runtime modules for Fantom to its own package.

Reviewed By: javache

Differential Revision: D66825478

fbshipit-source-id: ac4dbc23b86895f09abc46345d497c1c53737ae2
2024-12-06 05:24:31 -08:00
Riccardo Cipolleschi 8d3c9ec3a1 Exclude mapping generation of core component (#48145)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48145

While writing the docs for 0.77, I found an edge case in the generation of the RCTThirdPartyComponentProvider:
* If the app has the `codegenConfig` field set in the `package.json`
* And it does not have the `ios.componentProvider` field is not provided

Codegen was generating the mapping for the react-native core components. That's not expected as, in that case, it should only generate components that are declared in the app or in libraries.

This change fixes this edge case.

## Changelog:
[Internal] - Exclude mapping generation of core component

Reviewed By: blakef

Differential Revision: D66875080

fbshipit-source-id: 65fe10381729ec7808efec70feacf2a55f0056e9
2024-12-06 05:05:48 -08: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
356 changed files with 12454 additions and 6488 deletions
+1
View File
@@ -3,6 +3,7 @@
docs/generatedComponentApiDocs.js
packages/react-native/flow/
packages/react-native/sdks/
packages/react-native/ReactAndroid/build
packages/react-native/ReactAndroid/hermes-engine/build/
packages/react-native/Libraries/Renderer/*
packages/react-native/Libraries/vendor/**/*
+1 -2
View File
@@ -44,7 +44,6 @@ packages/react-native/flow/
[options]
enums=true
as_const=true
casting_syntax=both
emoji=true
@@ -96,4 +95,4 @@ untyped-import
untyped-type-import
[version]
^0.255.0
^0.256.0
@@ -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 [...]
+3
View File
@@ -30,6 +30,9 @@ runs:
- name: Lint java
shell: bash
run: ./scripts/circleci/exec_swallow_error.sh yarn lint-java --check
- name: Verify not committing repo after running build
shell: bash
run: yarn run build --check
- name: Run flowcheck
shell: bash
run: yarn flow-check
@@ -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 }}
+1 -1
View File
@@ -157,4 +157,4 @@ vendor/
.circleci/generated_config.yml
# Jest Integration
/jest/integration/build/
/packages/react-native-fantom/build/
+46
View File
@@ -1,4 +1,29 @@
# Changelog
## v0.77.0-rc.2
### Changed
- Reverts #47503. (~~Callbacks passed to `animation.start(<callback>)` will be scheduled for execution in a microtask. Previously, there were certain scenarios in which the callback could be synchronously executed by `start`.~~) ([8793b7d89b](https://github.com/facebook/react-native/commit/8793b7d89bcafdfcca7ecb953e60882b67ffc807) by [@yungsters](https://github.com/yungsters))
### Fixed
#### Android specific
- Fix crash on HeadlessJsTaskService on old architecture ([4560fc0497](https://github.com/facebook/react-native/commit/4560fc049748a345d5945bc08d43f4b61ca51ff3) by [@cortinico](https://github.com/cortinico))
- Re-introduce the deprecated constructor on ReactModuleInfo ([734730df75](https://github.com/facebook/react-native/commit/734730df75b3bdddeb5dbe65f4151cc92b988303) by [@cortinico](https://github.com/cortinico))
## 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
@@ -382,6 +407,27 @@
- Solved SVC warnings for RNTester ([fad4a0783b](https://github.com/facebook/react-native/commit/fad4a0783b0a0478c147d9bde2ef9ab082a08297) by [@cipolleschi](https://github.com/cipolleschi))
- Don't reference PrivacyInfo.xcprivacy twice for new projects ([cadd41b1a2](https://github.com/facebook/react-native/commit/cadd41b1a2e16b1c77a8d3022f4ccbdbd5ea295f) by [@okwasniewski](https://github.com/okwasniewski))
## v0.76.5
### Fixed
- Better support filtering out non linked platforms ([fcbcf80d1c](https://github.com/facebook/react-native/commit/fcbcf80d1c080af42b5277fc8a153059194efb95) by [@cipolleschi](https://github.com/cipolleschi))
#### Android specific
- Fix crash on HeadlessJsTaskService on old architecture ([4560fc0497](https://github.com/facebook/react-native/commit/4560fc049748a345d5945bc08d43f4b61ca51ff3) by [@cortinico](https://github.com/cortinico))
## v0.76.4
### Added
- Sync debugger-frontend to latest 0.76-stable (fix Expo node_modules entry points in Sources panel) ([43fe69c315](https://github.com/facebook/react-native/commit/43fe69c315e68aab96c303c7a6c9b3821a6e25e5) by [@huntie](https://github.com/huntie))
- Exclude unlinked libs from codegen ([3cedb09a65](https://github.com/facebook/react-native/commit/3cedb09a650adda0b3f24e931c25f27730af19b1) by [@cipolleschi](https://github.com/cipolleschi))
#### Android specific
- Avoid NPE when touch event is triggered before SurfaceManager is initiated ([b8095f4692](https://github.com/facebook/react-native/commit/b8095f4692610c7f4631b851dc7d8dc9b149a277) by [@CHOIMINSEOK](https://github.com/CHOIMINSEOK))
## v0.76.3
### Fixed
+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
+77
View File
@@ -0,0 +1,77 @@
/**
* 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
* @format
* @oncall react_native
*/
declare module 'jest-snapshot' {
type SnapshotFormat = {...};
type UpdateSnapshot = 'all' | 'new' | 'none';
type ProjectConfig = {
rootDir: string,
prettierPath: string,
snapshotFormat: SnapshotFormat,
...
};
type SnapshotResolver = {
/** Resolves from `testPath` to snapshot path. */
resolveSnapshotPath(testPath: string, snapshotExtension?: string): string,
/** Resolves from `snapshotPath` to test path. */
resolveTestPath(snapshotPath: string, snapshotExtension?: string): string,
/** Example test path, used for preflight consistency check of the implementation above. */
testPathForConsistencyCheck: string,
};
declare export var EXTENSION: 'snap';
declare export function isSnapshotPath(path: string): boolean;
type LocalRequire = (module: string) => mixed;
declare export function buildSnapshotResolver(
config: ProjectConfig,
localRequire?: Promise<LocalRequire> | LocalRequire,
): Promise<SnapshotResolver>;
type SnapshotStateOptions = {
updateSnapshot: UpdateSnapshot,
prettierPath?: string | null,
expand?: boolean,
snapshotFormat: SnapshotFormat,
rootDir: string,
};
type SnapshotData = Record<string, string>;
type SaveStatus = {
deleted: boolean,
saved: boolean,
};
declare export class SnapshotState {
_dirty: boolean;
_updateSnapshot: UpdateSnapshot;
_snapshotData: SnapshotData;
_initialData: SnapshotData;
_uncheckedKeys: Set<string>;
added: number;
expand: boolean;
matched: number;
unmatched: number;
updated: number;
constructor(testPath: string, options: SnapshotStateOptions): void;
save(): SaveStatus;
getUncheckedCount(): number;
getUncheckedKeys(): Array<string>;
removeUncheckedKeys(): void;
}
}
-1
View File
@@ -37,7 +37,6 @@ module.exports = {
'/node_modules/',
'<rootDir>/packages/react-native/sdks',
'<rootDir>/packages/react-native/Libraries/Renderer',
'<rootDir>/packages/react-native-test-renderer/src',
'<rootDir>/packages/react-native/sdks/hermes/',
...PODS_LOCATIONS,
],
-451
View File
@@ -1,451 +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 deepEqual from 'deep-equal';
import nullthrows from 'nullthrows';
export type TestCaseResult = {
ancestorTitles: Array<string>,
title: string,
fullName: string,
status: 'passed' | 'failed' | 'pending',
duration: number,
failureMessages: Array<string>,
numPassingAsserts: number,
// location: string,
};
export type TestSuiteResult =
| {
testResults: Array<TestCaseResult>,
}
| {
error: {
message: string,
stack: string,
},
};
const tests: Array<{
title: string,
ancestorTitles: Array<string>,
implementation: () => mixed,
isFocused: boolean,
isSkipped: boolean,
result?: TestCaseResult,
}> = [];
const ancestorTitles: Array<string> = [];
const globalModifiers: Array<'focused' | 'skipped'> = [];
const globalDescribe = (global.describe = (
title: string,
implementation: () => mixed,
) => {
ancestorTitles.push(title);
implementation();
ancestorTitles.pop();
});
const globalIt =
(global.it =
global.test =
(title: string, implementation: () => mixed) =>
tests.push({
title,
implementation,
ancestorTitles: ancestorTitles.slice(),
isFocused:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused',
isSkipped:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'skipped',
}));
// $FlowExpectedError[prop-missing]
global.fdescribe = global.describe.only = (
title: string,
implementation: () => mixed,
) => {
globalModifiers.push('focused');
globalDescribe(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.it.only =
global.fit =
// $FlowExpectedError[prop-missing]
global.test.only =
(title: string, implementation: () => mixed) => {
globalModifiers.push('focused');
globalIt(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.xdescribe = global.describe.skip = (
title: string,
implementation: () => mixed,
) => {
globalModifiers.push('skipped');
globalDescribe(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.it.skip =
global.xit =
// $FlowExpectedError[prop-missing]
global.test.skip =
global.xtest =
(title: string, implementation: () => mixed) => {
globalModifiers.push('skipped');
globalIt(title, implementation);
globalModifiers.pop();
};
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);
function runWithGuard(fn: () => void) {
try {
fn();
} catch (error) {
let reportedError =
error instanceof Error ? error : new Error(String(error));
reportTestSuiteResult({
error: {
message: reportedError.message,
stack: reportedError.stack,
},
});
}
}
function executeTests() {
const hasFocusedTests = tests.some(test => test.isFocused);
for (const test of tests) {
const result: TestCaseResult = {
title: test.title,
fullName: [...test.ancestorTitles, test.title].join(' '),
ancestorTitles: test.ancestorTitles,
status: 'pending',
duration: 0,
failureMessages: [],
numPassingAsserts: 0,
};
test.result = result;
if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
let status;
let error;
const start = Date.now();
try {
test.implementation();
status = 'passed';
} catch (e) {
error = e;
status = 'failed';
}
result.status = status;
result.duration = Date.now() - start;
result.failureMessages =
status === 'failed' && error
? [error.stack ?? error.message ?? String(error)]
: [];
}
}
reportTestSuiteResult({
testResults: tests.map(test => nullthrows(test.result)),
});
}
function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
console.log(JSON.stringify(testSuiteResult));
}
global.$$RunTests$$ = () => {
executeTests();
};
export function registerTest(setUpTest: () => void) {
runWithGuard(() => {
setUpTest();
});
}
+3 -1
View File
@@ -31,6 +31,7 @@
"test-typescript-offline": "dtslint --localTs node_modules/typescript/lib packages/react-native/types",
"test-typescript": "dtslint packages/react-native/types",
"test": "jest",
"fantom": "JS_DIR='..' yarn jest --config packages/react-native-fantom/config/jest.config.js",
"trigger-react-native-release": "node ./scripts/releases-local/trigger-react-native-release.js",
"update-lock": "npx yarn-deduplicate"
},
@@ -77,13 +78,14 @@
"eslint-plugin-redundant-undefined": "^0.4.0",
"eslint-plugin-relay": "^1.8.3",
"flow-api-translator": "0.25.1",
"flow-bin": "^0.255.0",
"flow-bin": "^0.256.0",
"glob": "^7.1.1",
"hermes-eslint": "0.25.1",
"hermes-transform": "0.25.1",
"inquirer": "^7.1.0",
"jest": "^29.6.3",
"jest-diff": "^29.7.0",
"jest-snapshot": "^29.7.0",
"jest-junit": "^10.0.0",
"jscodeshift": "^0.14.0",
"metro-babel-register": "^0.81.0",
@@ -1,50 +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.tasks.internal
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A task that takes care of unbundling JSC and preparing it for be consumed by the Android NDK.
* Specifically it will unbundle shared libs, headers and will copy over the Makefile from
* `src/main/jni/third-party/jsc/`
*/
abstract class PrepareJSCTask : DefaultTask() {
@get:Input abstract val jscPackagePath: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@TaskAction
fun taskAction() {
if (!jscPackagePath.isPresent || jscPackagePath.orNull == null) {
error("Could not find the jsc-android npm package")
}
val jscDist = File(jscPackagePath.get(), "dist")
if (!jscDist.exists()) {
error("The jsc-android npm package is missing its \"dist\" directory")
}
val jscAAR =
project.fileTree(jscDist).matching { it.include("**/android-jsc/**/*.aar") }.singleFile
val soFiles = project.zipTree(jscAAR).matching { it.include("**/*.so") }
val headerFiles = project.fileTree(jscDist).matching { it.include("**/include/*.h") }
project.copy { it ->
it.from(soFiles)
it.from(headerFiles)
it.from(project.file("src/main/jni/third-party/jsc/CMakeLists.txt"))
it.filesMatching("**/*.h") { it.path = "JavaScriptCore/${it.name}" }
it.includeEmptyDirs = false
it.into(outputDir)
}
}
}
@@ -47,14 +47,11 @@ internal object DependencyUtils {
repo.content { it.excludeGroup("com.facebook.react") }
}
}
// Android JSC is installed from npm
mavenRepoFromURI(File(reactNativeDir, "../jsc-android/dist").toURI()) { repo ->
repo.content { it.includeGroup("org.webkit") }
}
repositories.google { repo ->
repo.content {
// We don't want to fetch JSC or React from Google
it.excludeGroup("org.webkit")
it.excludeGroup("io.github.react-native-community")
it.excludeGroup("com.facebook.react")
}
}
@@ -62,6 +59,7 @@ internal object DependencyUtils {
repo.content {
// We don't want to fetch JSC or React from JitPack
it.excludeGroup("org.webkit")
it.excludeGroup("io.github.react-native-community")
it.excludeGroup("com.facebook.react")
}
}
@@ -1,129 +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.tasks.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import com.facebook.react.tests.zipFiles
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PrepareJSCTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun prepareJSCTask_withMissingPackage_fails() {
val task = createTestTask<PrepareJSCTask>()
assertThatThrownBy { task.taskAction() }.isInstanceOf(IllegalStateException::class.java)
}
@Test
fun prepareJSCTask_withNullPackage_fails() {
val task = createTestTask<PrepareJSCTask> { it.jscPackagePath.set(null as String?) }
assertThatThrownBy { task.taskAction() }.isInstanceOf(IllegalStateException::class.java)
}
@Test
fun prepareJSCTask_withMissingDistFolder_fails() {
val task =
createTestTask<PrepareJSCTask> { it.jscPackagePath.set(tempFolder.root.absolutePath) }
assertThatThrownBy { task.taskAction() }.isInstanceOf(IllegalStateException::class.java)
}
@Test
fun prepareJSCTask_ignoresEmptyDirs() {
prepareInputFolder()
val output = tempFolder.newFolder("output")
File(tempFolder.root, "dist/just/an/empty/folders/").apply { mkdirs() }
val task =
createTestTask<PrepareJSCTask> {
it.jscPackagePath.set(tempFolder.root.absolutePath)
it.outputDir.set(output)
}
task.taskAction()
assertThat(File(output, "just/an/empty/folders/")).doesNotExist()
}
@Test
fun prepareJSCTask_copiesSoFiles() {
val soFile = tempFolder.newFile("libsomething.so")
prepareInputFolder(aarContent = listOf(soFile))
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareJSCTask> {
it.jscPackagePath.set(tempFolder.root.absolutePath)
it.outputDir.set(output)
}
task.taskAction()
assertThat(output.listFiles()?.first()?.name).isEqualTo("libsomething.so")
}
@Test
fun prepareJSCTask_copiesHeaderFilesToCorrectFolder() {
prepareInputFolder()
File(tempFolder.root, "dist/include/justaheader.h").apply {
parentFile.mkdirs()
createNewFile()
}
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareJSCTask> {
it.jscPackagePath.set(tempFolder.root.absolutePath)
it.outputDir.set(output)
}
task.taskAction()
assertThat(File(output, "JavaScriptCore/justaheader.h")).exists()
}
@Test
fun prepareJSCTask_copiesCMakefile() {
val project = createProject()
prepareInputFolder()
File(project.projectDir, "src/main/jni/third-party/jsc/CMakeLists.txt").apply {
parentFile.mkdirs()
createNewFile()
}
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareJSCTask>(project = project) {
it.jscPackagePath.set(tempFolder.root.absolutePath)
it.outputDir.set(output)
}
task.taskAction()
assertThat(File(output, "CMakeLists.txt")).exists()
}
private fun prepareInputFolder(aarContent: List<File> = listOf(tempFolder.newFile())) {
val dist = tempFolder.newFolder("dist")
File(dist, "android-jsc/android-library.aar").apply {
parentFile.mkdirs()
createNewFile()
}
zipFiles(File(dist, "android-jsc/android-library.aar"), aarContent)
}
}
@@ -56,23 +56,6 @@ class DependencyUtilsTest {
.isNotNull()
}
@Test
fun configureRepositories_containsJscLocalMavenRepo() {
val projectFolder = tempFolder.newFolder()
val reactNativeDir = tempFolder.newFolder("react-native")
val jscAndroidDir = tempFolder.newFolder("jsc-android")
val repositoryURI = URI.create("file://${jscAndroidDir}/dist")
val project = createProject(projectFolder)
configureRepositories(project, reactNativeDir)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
})
.isNotNull()
}
@Test
fun configureRepositories_containsMavenCentral() {
val repositoryURI = URI.create("https://repo.maven.apache.org/maven2/")
+2 -2
View File
@@ -81,14 +81,14 @@ def enableProguardInReleaseBuilds = false
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
* `def jscFlavor = "io.github.react-native-community:jsc-android-intl:2026004.+"`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
def jscFlavor = "io.github.react-native-community:jsc-android:2026004.+"
android {
ndkVersion rootProject.ext.ndkVersion
+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);
+27 -23
View File
@@ -8,20 +8,31 @@
*/
'use strict';
const {danger, fail, /*message,*/ warn} = require('danger');
const includes = require('lodash.includes');
const {danger, fail, warn} = require('danger');
const isFromPhabricator =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('differential revision:');
const body = danger.github.pr.body?.toLowerCase() ?? '';
function body_contains(...text) {
for (const matcher of text) {
if (body.includes(matcher)) {
return true;
}
}
return false;
}
const isFromPhabricator = body_contains('differential revision:');
// Provides advice if a summary section is missing, or body is too short
const includesSummary =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('## summary');
if (!danger.github.pr.body || danger.github.pr.body.length < 50) {
const includesSummary = body_contains('## summary', 'summary:');
const hasNoUsefulBody =
!danger.github.pr.body || danger.github.pr.body.length < 50;
const hasTooShortAHumanSummary =
!includesSummary && body.split('\n').length <= 2 && !isFromPhabricator;
if (hasNoUsefulBody) {
fail(':grey_question: This pull request needs a description.');
} else if (!includesSummary && !isFromPhabricator) {
} else if (hasTooShortAHumanSummary) {
// PRs from Phabricator always includes the Summary by default.
const title = ':clipboard: Missing Summary';
const idea =
@@ -31,20 +42,13 @@ if (!danger.github.pr.body || danger.github.pr.body.length < 50) {
warn(`${title} - <i>${idea}</i>`);
}
// Warns if there are changes to package.json, and tags the team.
const packageChanged = includes(danger.git.modified_files, 'package.json');
if (packageChanged) {
const title = ':lock: package.json';
const idea =
'Changes were made to package.json. ' +
'This will require a manual import by a Facebook employee.';
warn(`${title} - <i>${idea}</i>`);
}
// Provides advice if a test plan is missing.
const includesTestPlan =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('## test plan');
const includesTestPlan = body_contains(
'## test plan',
'test plan:',
'tests:',
'test:',
);
if (!includesTestPlan && !isFromPhabricator) {
// PRs from Phabricator never exports the Test Plan so let's disable this check.
const title = ':clipboard: Missing Test Plan';
+1
View File
@@ -430,4 +430,5 @@ export type CompleteTypeAnnotation =
| NativeModuleFunctionTypeAnnotation
| NullableTypeAnnotation<NativeModuleTypeAnnotation>
| EventEmitterTypeAnnotation
| NativeModuleEnumDeclarationWithMembers
| UnsafeAnyTypeAnnotation;
@@ -12,18 +12,25 @@
const baseConfig = require('../../../jest.config');
const path = require('path');
const isCI = Boolean(process.env.SANDCASTLE || process.env.GITHUB_ACTIONS);
module.exports = {
rootDir: path.resolve(__dirname, '../../..'),
roots: [
'<rootDir>/packages/react-native',
'<rootDir>/jest/integration/runtime',
'<rootDir>/packages/react-native-fantom',
],
moduleFileExtensions: [...baseConfig.moduleFileExtensions, 'cpp', 'h'],
// This allows running Meta-internal tests with the `-test.fb.js` suffix.
testRegex: '/__tests__/.*-itest(\\.fb)?\\.js$',
testPathIgnorePatterns: baseConfig.testPathIgnorePatterns,
transformIgnorePatterns: ['.*'],
testRunner: './jest/integration/runner/index.js',
watchPathIgnorePatterns: ['<rootDir>/jest/integration/build/'],
globalSetup: './jest/integration/runner/warmup/index.js',
testRunner: '<rootDir>/packages/react-native-fantom/runner/index.js',
watchPathIgnorePatterns: ['<rootDir>/packages/react-native-fantom/build/'],
// In CI, we want to prewarm the caches/builds before running the tests so
// that time isn't attributed to the first test that runs.
globalSetup: isCI
? '<rootDir>/packages/react-native-fantom/runner/warmup/index.js'
: null,
};
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@react-native/fantom",
"private": true,
"version": "0.77.0-main",
"main": "src/index.js",
"description": "Internal integration testing and benchmarking tool for React Native",
"peerDependencies":{
"jest":"^29.7.0",
"jest-snapshot": "^29.7.0"
}
}
@@ -9,12 +9,21 @@
* @oncall react_native
*/
import type {SnapshotConfig} from '../runtime/snapshotContext';
import type {FantomTestConfigJsOnlyFeatureFlags} from './getFantomTestConfig';
module.exports = function entrypointTemplate({
testPath,
setupModulePath,
featureFlagsModulePath,
featureFlags,
snapshotConfig,
}: {
testPath: string,
setupModulePath: string,
featureFlagsModulePath: string,
featureFlags: FantomTestConfigJsOnlyFeatureFlags,
snapshotConfig: SnapshotConfig,
}): string {
return `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -29,7 +38,18 @@ module.exports = function entrypointTemplate({
*/
import {registerTest} from '${setupModulePath}';
${
Object.keys(featureFlags).length > 0
? `import * as ReactNativeFeatureFlags from '${featureFlagsModulePath}';
registerTest(() => require('${testPath}'));
ReactNativeFeatureFlags.override({
${Object.entries(featureFlags)
.map(([name, value]) => ` ${name}: () => ${JSON.stringify(value)},`)
.join('\n')}
});`
: ''
}
registerTest(() => require('${testPath}'), ${JSON.stringify(snapshotConfig)});
`;
};
@@ -0,0 +1,179 @@
/**
* 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 enum FantomTestConfigMode {
DevelopmentWithBytecode,
DevelopmentWithSource,
Optimized,
}
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 =
FantomTestConfigMode.DevelopmentWithSource;
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;
switch (mode) {
case 'dev':
config.mode = FantomTestConfigMode.DevelopmentWithSource;
break;
case 'dev-bytecode':
config.mode = FantomTestConfigMode.DevelopmentWithBytecode;
break;
case 'opt':
config.mode = FantomTestConfigMode.Optimized;
break;
default:
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}`);
}
}
@@ -12,10 +12,15 @@
import type {TestSuiteResult} from '../runtime/setup';
import entrypointTemplate from './entrypoint-template';
import getFantomTestConfig from './getFantomTestConfig';
import {FantomTestConfigMode} from './getFantomTestConfig';
import {
getInitialSnapshotData,
updateSnapshotsAndGetJestSnapshotResult,
} from './snapshotUtils';
import {
getBuckModeForPlatform,
getDebugInfoFromCommandResult,
getFantomTestConfig,
getShortHash,
runBuck2,
symbolicateStackTrace,
@@ -23,6 +28,7 @@ import {
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {formatResultsErrors} from 'jest-message-util';
import {SnapshotState, buildSnapshotResolver} from 'jest-snapshot';
import Metro from 'metro';
import nullthrows from 'nullthrows';
import path from 'path';
@@ -67,19 +73,21 @@ function generateBytecodeBundle({
bytecodePath: string,
isOptimizedMode: boolean,
}): void {
const hermesCompilerCommandResult = runBuck2([
'run',
getBuckModeForPlatform(isOptimizedMode),
'//xplat/hermes/tools/hermesc:hermesc',
'--',
'-emit-binary',
'-O',
'-max-diagnostic-width',
'80',
'-out',
bytecodePath,
sourcePath,
]);
const hermesCompilerCommandResult = runBuck2(
[
'run',
getBuckModeForPlatform(isOptimizedMode),
'//xplat/hermes/tools/hermesc:hermesc',
'--',
'-emit-binary',
isOptimizedMode ? '-O' : null,
'-max-diagnostic-width',
'80',
'-out',
bytecodePath,
sourcePath,
].filter(Boolean),
);
if (hermesCompilerCommandResult.status !== 0) {
throw new Error(getDebugInfoFromCommandResult(hermesCompilerCommandResult));
@@ -87,35 +95,59 @@ function generateBytecodeBundle({
}
module.exports = async function runTest(
globalConfig: {...},
config: {...},
globalConfig: {
updateSnapshot: 'all' | 'new' | 'none',
...
},
config: {
rootDir: string,
prettierPath: string,
snapshotFormat: {...},
...
},
environment: {...},
runtime: {...},
testPath: string,
): mixed {
const snapshotResolver = await buildSnapshotResolver(config);
const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath);
const snapshotState = new SnapshotState(snapshotPath, {
updateSnapshot: globalConfig.updateSnapshot,
snapshotFormat: config.snapshotFormat,
prettierPath: config.prettierPath,
rootDir: config.rootDir,
});
const startTime = Date.now();
const testConfig = getFantomTestConfig(testPath);
const isOptimizedMode = testConfig.mode === 'opt';
const metroConfig = await Metro.loadConfig({
config: path.resolve(__dirname, '..', 'config', 'metro.config.js'),
});
const setupModulePath = path.resolve(__dirname, '../runtime/setup.js');
const featureFlagsModulePath = path.resolve(
__dirname,
'../../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,
snapshotConfig: {
updateSnapshot: snapshotState._updateSnapshot,
data: getInitialSnapshotData(snapshotState),
},
});
const entrypointPath = path.join(
BUILD_OUTPUT_PATH,
`${getShortHash(entrypointContents)}-${path.basename(testPath)}`,
);
const testBundlePath = entrypointPath + '.bundle';
const testJSBundlePath = testBundlePath + '.js';
const testJSBundlePath = entrypointPath + '.bundle.js';
const testBytecodeBundlePath = testJSBundlePath + '.hbc';
fs.mkdirSync(path.dirname(entrypointPath), {recursive: true});
@@ -130,27 +162,31 @@ module.exports = async function runTest(
entry: entrypointPath,
out: testJSBundlePath,
platform: 'android',
minify: isOptimizedMode,
dev: !isOptimizedMode,
minify: testConfig.mode === FantomTestConfigMode.Optimized,
dev: testConfig.mode !== FantomTestConfigMode.Optimized,
sourceMap: true,
sourceMapUrl: sourceMapPath,
});
if (isOptimizedMode) {
if (testConfig.mode !== FantomTestConfigMode.DevelopmentWithSource) {
generateBytecodeBundle({
sourcePath: testJSBundlePath,
bytecodePath: testBytecodeBundlePath,
isOptimizedMode,
isOptimizedMode: testConfig.mode === FantomTestConfigMode.Optimized,
});
}
const rnTesterCommandResult = runBuck2([
'run',
getBuckModeForPlatform(isOptimizedMode),
getBuckModeForPlatform(testConfig.mode === FantomTestConfigMode.Optimized),
'//xplat/ReactNative/react-native-cxx/samples/tester:tester',
'--',
'--bundlePath',
testBundlePath,
testConfig.mode === FantomTestConfigMode.DevelopmentWithSource
? testJSBundlePath
: testBytecodeBundlePath,
'--featureFlags',
JSON.stringify(testConfig.flags.common),
]);
if (rnTesterCommandResult.status !== 0) {
@@ -189,6 +225,15 @@ module.exports = async function runTest(
),
})) ?? [];
const snapshotResults = nullthrows(
rnTesterParsedOutput.testResult.testResults,
).map(testResult => testResult.snapshotResults);
const snapshotResult = updateSnapshotsAndGetJestSnapshotResult(
snapshotState,
snapshotResults,
);
return {
testFilePath: testPath,
failureMessage: formatResultsErrors(
@@ -206,15 +251,7 @@ module.exports = async function runTest(
runtime: endTime - startTime,
slow: false,
},
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0,
},
snapshot: snapshotResult,
numTotalTests: testResults.length,
numPassingTests: testResults.filter(test => test.status === 'passed')
.length,
+100
View File
@@ -0,0 +1,100 @@
/**
* 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 type {TestSnapshotResults} from '../runtime/snapshotContext';
import type {SnapshotState} from 'jest-snapshot';
type JestSnapshotResult = {
added: number,
fileDeleted: boolean,
matched: number,
unchecked: number,
uncheckedKeys: string[],
unmatched: number,
updated: number,
};
// Add extra line breaks at beginning and end of multiline snapshot
// to make the content easier to read.
const addExtraLineBreaks = (string: string): string =>
string.includes('\n') ? `\n${string}\n` : string;
// Remove extra line breaks at beginning and end of multiline snapshot.
// Instead of trim, which can remove additional newlines or spaces
// at beginning or end of the content from a custom serializer.
const removeExtraLineBreaks = (string: string): string =>
string.length > 2 && string.startsWith('\n') && string.endsWith('\n')
? string.slice(1, -1)
: string;
export const getInitialSnapshotData = (
snapshotState: SnapshotState,
): {[key: string]: string} => {
const initialData: {[key: string]: string} = {};
for (const key in snapshotState._initialData) {
initialData[key] = removeExtraLineBreaks(snapshotState._initialData[key]);
}
return initialData;
};
export const updateSnapshotsAndGetJestSnapshotResult = (
snapshotState: SnapshotState,
testSnapshotResults: Array<TestSnapshotResults>,
): JestSnapshotResult => {
for (const snapshotResults of testSnapshotResults) {
for (const [key, result] of Object.entries(snapshotResults)) {
if (result.pass) {
snapshotState.matched++;
snapshotState._uncheckedKeys.delete(key);
continue;
}
if (snapshotState._snapshotData[key] === undefined) {
if (snapshotState._updateSnapshot === 'none') {
snapshotState.unmatched++;
continue;
}
snapshotState._dirty = true;
snapshotState._snapshotData[key] = addExtraLineBreaks(result.value);
snapshotState.added++;
snapshotState.matched++;
snapshotState._uncheckedKeys.delete(key);
continue;
}
snapshotState._dirty = true;
snapshotState._snapshotData[key] = addExtraLineBreaks(result.value);
snapshotState.updated++;
snapshotState._uncheckedKeys.delete(key);
}
}
const uncheckedCount = snapshotState.getUncheckedCount();
const uncheckedKeys = snapshotState.getUncheckedKeys();
if (uncheckedCount) {
snapshotState.removeUncheckedKeys();
}
const status = snapshotState.save();
return {
added: snapshotState.added,
fileDeleted: status.deleted,
matched: snapshotState.matched,
unchecked: status.deleted ? 0 : snapshotState.getUncheckedCount(),
uncheckedKeys: [...uncheckedKeys],
unmatched: snapshotState.unmatched,
updated: snapshotState.updated,
};
};
@@ -12,59 +12,11 @@
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;
}
const BUCK_ISOLATION_DIR = 'react-native-fantom-buck-out';
export function getBuckModeForPlatform(enableRelease: boolean = false): string {
const mode = enableRelease ? 'opt' : 'dev';
@@ -90,6 +42,16 @@ type SpawnResultWithOriginalCommand = {
};
export function runBuck2(args: Array<string>): SpawnResultWithOriginalCommand {
// If these tests are already running from withing a buck2 process, e.g. when
// they are scheduled by a `buck2 test` wrapper, calling `buck2` again would
// cause a daemon-level deadlock.
// To prevent this - explicitly pass custom `--isolation-dir`. Reuse the same
// dir across tests (even running in different jest processes) to properly
// employ caching.
if (process.env.BUCK2_WRAPPER != null) {
args.unshift('--isolation-dir', BUCK_ISOLATION_DIR);
}
const result = spawnSync('buck2', args, {
encoding: 'utf8',
env: {
@@ -20,26 +20,49 @@ import Metro from 'metro';
import os from 'os';
import path from 'path';
export default async function warmUp(
globalConfig: {...},
projectConfig: {...},
async function tryOrLog(
fn: () => void | Promise<void>,
message: string,
): Promise<void> {
try {
warmUpHermesCompiler();
warmUpRNTesterCLI();
await warmUpMetro();
await fn();
} catch (e) {
// Sandcastle fails to parse the test output if we log stuff to stdout/stderr.
if ((process.env.SANDCASTLE ?? '') !== '') {
console.error(
'Global warmup failed. Tests will continue to run but will likely fail. Details:\n',
message,
e,
);
}
}
}
async function warmUpMetro(): Promise<void> {
export default async function warmUp(
globalConfig: {...},
projectConfig: {...},
): Promise<void> {
await tryOrLog(
() => warmUpHermesCompiler(false),
'Error warming up Hermes compiler (dev)',
);
await tryOrLog(
() => warmUpHermesCompiler(true),
'Error warming up Hermes compiler (opt)',
);
await tryOrLog(
() => warmUpRNTesterCLI(false),
'Error warming up RN Tester CLI (dev)',
);
await tryOrLog(
() => warmUpRNTesterCLI(true),
'Error warming up RN Tester CLI (opt)',
);
await tryOrLog(() => warmUpMetro(false), 'Error warming up Metro (dev)');
await tryOrLog(() => warmUpMetro(true), 'Error warming up Metro (opt)');
}
async function warmUpMetro(isOptimizedMode: boolean): Promise<void> {
const metroConfig = await Metro.loadConfig({
config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'),
});
@@ -61,8 +84,8 @@ async function warmUpMetro(): Promise<void> {
entry: entrypointPath,
out: bundlePath,
platform: 'android',
minify: false,
dev: true,
minify: isOptimizedMode,
dev: !isOptimizedMode,
});
try {
@@ -70,10 +93,10 @@ async function warmUpMetro(): Promise<void> {
} catch {}
}
function warmUpHermesCompiler(): void {
function warmUpHermesCompiler(isOptimizedMode: boolean): void {
const buildHermesCompilerCommandResult = runBuck2([
'build',
getBuckModeForPlatform(),
getBuckModeForPlatform(isOptimizedMode),
'//xplat/hermes/tools/hermesc:hermesc',
]);
@@ -84,10 +107,10 @@ function warmUpHermesCompiler(): void {
}
}
function warmUpRNTesterCLI(): void {
function warmUpRNTesterCLI(isOptimizedMode: boolean): void {
const buildRNTesterCommandResult = runBuck2([
'build',
getBuckModeForPlatform(),
getBuckModeForPlatform(isOptimizedMode),
'//xplat/ReactNative/react-native-cxx/samples/tester:tester',
]);
@@ -4,6 +4,7 @@
* 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
*/
@@ -13,5 +14,5 @@
*/
import 'react-native/Libraries/Core/InitializeCore.js';
import 'react-native/src/private/__tests__/ReactNativeTester';
import '@react-native/fantom/src/__tests__/Fantom-itest';
import './setup';
+297
View File
@@ -0,0 +1,297 @@
/**
* 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 {snapshotContext} from './snapshotContext';
import deepEqual from 'deep-equal';
import {diff} from 'jest-diff';
import {format, plugins} from 'pretty-format';
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();
}
}
toMatchSnapshot(expected?: string): void {
if (this.#isNot) {
throw new ErrorWithCustomBlame(
'Snapshot matchers cannot be used with not.',
).blameToPreviousFrame();
}
const receivedValue = format(this.#received, {
plugins: [plugins.ReactElement],
});
try {
snapshotContext.toMatchSnapshot(receivedValue, expected);
} catch (err) {
throw new ErrorWithCustomBlame(err.message).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;
}
+216
View File
@@ -0,0 +1,216 @@
/**
* 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 type {SnapshotConfig, TestSnapshotResults} from './snapshotContext';
import expect from './expect';
import {createMockFunction} from './mocks';
import {setupSnapshotConfig, snapshotContext} from './snapshotContext';
import nullthrows from 'nullthrows';
export type TestCaseResult = {
ancestorTitles: Array<string>,
title: string,
fullName: string,
status: 'passed' | 'failed' | 'pending',
duration: number,
failureMessages: Array<string>,
numPassingAsserts: number,
snapshotResults: TestSnapshotResults,
// location: string,
};
export type TestSuiteResult =
| {
testResults: Array<TestCaseResult>,
}
| {
error: {
message: string,
stack: string,
},
};
type SnapshotState = {
name: string,
snapshotResults: TestSnapshotResults,
};
let currentSnapshotState: SnapshotState;
const tests: Array<{
title: string,
ancestorTitles: Array<string>,
implementation: () => mixed,
isFocused: boolean,
isSkipped: boolean,
result?: TestCaseResult,
}> = [];
const ancestorTitles: Array<string> = [];
const globalModifiers: Array<'focused' | 'skipped'> = [];
const globalDescribe = (global.describe = (
title: string,
implementation: () => mixed,
) => {
ancestorTitles.push(title);
implementation();
ancestorTitles.pop();
});
const globalIt =
(global.it =
global.test =
(title: string, implementation: () => mixed) =>
tests.push({
title,
implementation,
ancestorTitles: ancestorTitles.slice(),
isFocused:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'focused',
isSkipped:
globalModifiers.length > 0 &&
globalModifiers[globalModifiers.length - 1] === 'skipped',
}));
// $FlowExpectedError[prop-missing]
global.fdescribe = global.describe.only = (
title: string,
implementation: () => mixed,
) => {
globalModifiers.push('focused');
globalDescribe(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.it.only =
global.fit =
// $FlowExpectedError[prop-missing]
global.test.only =
(title: string, implementation: () => mixed) => {
globalModifiers.push('focused');
globalIt(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.xdescribe = global.describe.skip = (
title: string,
implementation: () => mixed,
) => {
globalModifiers.push('skipped');
globalDescribe(title, implementation);
globalModifiers.pop();
};
// $FlowExpectedError[prop-missing]
global.it.skip =
global.xit =
// $FlowExpectedError[prop-missing]
global.test.skip =
global.xtest =
(title: string, implementation: () => mixed) => {
globalModifiers.push('skipped');
globalIt(title, implementation);
globalModifiers.pop();
};
global.jest = {
fn: createMockFunction,
};
global.expect = expect;
function runWithGuard(fn: () => void) {
try {
fn();
} catch (error) {
let reportedError =
error instanceof Error ? error : new Error(String(error));
reportTestSuiteResult({
error: {
message: reportedError.message,
stack: reportedError.stack,
},
});
}
}
function executeTests() {
const hasFocusedTests = tests.some(test => test.isFocused);
for (const test of tests) {
const result: TestCaseResult = {
title: test.title,
fullName: [...test.ancestorTitles, test.title].join(' '),
ancestorTitles: test.ancestorTitles,
status: 'pending',
duration: 0,
failureMessages: [],
numPassingAsserts: 0,
snapshotResults: {},
};
test.result = result;
snapshotContext.setTargetTest(result.fullName);
if (!test.isSkipped && (!hasFocusedTests || test.isFocused)) {
let status;
let error;
const start = Date.now();
try {
test.implementation();
status = 'passed';
} catch (e) {
error = e;
status = 'failed';
}
result.status = status;
result.duration = Date.now() - start;
result.failureMessages =
status === 'failed' && error
? [error.stack ?? error.message ?? String(error)]
: [];
result.snapshotResults = snapshotContext.getSnapshotResults();
}
}
reportTestSuiteResult({
testResults: tests.map(test => nullthrows(test.result)),
});
}
function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
console.log(JSON.stringify(testSuiteResult));
}
global.$$RunTests$$ = () => {
executeTests();
};
export function registerTest(
setUpTest: () => void,
snapshotConfig: SnapshotConfig,
) {
setupSnapshotConfig(snapshotConfig);
runWithGuard(() => {
setUpTest();
});
}
+113
View File
@@ -0,0 +1,113 @@
/**
* 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 {diff} from 'jest-diff';
export type SnapshotConfig = {
updateSnapshot: 'all' | 'new' | 'none',
data: {[key: string]: string},
};
export type TestSnapshotResults = {
[key: string]:
| {
pass: true,
}
| {
pass: false,
value: string,
},
};
const COMPARISON_EQUALS_STRING = 'Compared values have no visual difference.';
let snapshotConfig: ?SnapshotConfig;
type SnapshotState = {
callCount: number,
testFullName: string,
snapshotResults: TestSnapshotResults,
};
class SnapshotContext {
#snapshotState: ?SnapshotState = null;
setTargetTest(testFullName: string) {
this.#snapshotState = {
callCount: 0,
testFullName,
snapshotResults: {},
};
}
toMatchSnapshot(received: string, label: ?string): void {
const snapshotState = this.#snapshotState;
if (snapshotState == null) {
throw new Error(
'Snapshot state is not set, call `setTargetTest()` first',
);
}
const snapshotKey = `${snapshotState.testFullName}${
label != null ? `: ${label}` : ''
} ${++snapshotState.callCount}`;
if (snapshotConfig == null) {
throw new Error(
'Snapshot config is not set. Did you forget to call `setupSnapshotConfig`?',
);
}
const updateSnapshot = snapshotConfig.updateSnapshot;
const snapshot = snapshotConfig.data[snapshotKey];
if (snapshot == null) {
snapshotState.snapshotResults[snapshotKey] = {
pass: false,
value: received,
};
if (updateSnapshot === 'none') {
throw new Error(
`Expected to have snapshot \`${snapshotKey}\` but it was not found.`,
);
}
return;
}
const result = diff(snapshot, received) ?? 'Failed to compare output';
if (result !== COMPARISON_EQUALS_STRING) {
snapshotState.snapshotResults[snapshotKey] = {
pass: false,
value: received,
};
if (updateSnapshot !== 'all') {
throw new Error(`Expected to match snapshot.\n${result}`);
}
return;
}
snapshotState.snapshotResults[snapshotKey] = {pass: true};
}
getSnapshotResults(): TestSnapshotResults {
return {...this.#snapshotState?.snapshotResults};
}
}
export const snapshotContext: SnapshotContext = new SnapshotContext();
export function setupSnapshotConfig(config: SnapshotConfig) {
snapshotConfig = config;
}
@@ -0,0 +1,271 @@
/**
* 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 'react-native/Libraries/Core/InitializeCore';
import {createRoot, runTask} from '..';
import * as React from 'react';
import {Text, View} from 'react-native';
describe('Fantom', () => {
describe('runTask', () => {
it('should run a task synchronously', () => {
const task = jest.fn();
runTask(task);
expect(task).toHaveBeenCalledTimes(1);
});
// TODO: fix error handling and make this pass
it.skip('should re-throw errors from the task synchronously', () => {
expect(() => {
runTask(() => {
throw new Error('test error');
});
}).toThrow('test error');
});
it('should exhaust the microtask queue synchronously', () => {
const lastMicrotask = jest.fn();
runTask(() => {
queueMicrotask(() => {
queueMicrotask(() => {
queueMicrotask(() => {
queueMicrotask(lastMicrotask);
});
});
});
});
expect(lastMicrotask).toHaveBeenCalledTimes(1);
});
// TODO: fix error handling and make this pass
it.skip('should re-throw errors from microtasks synchronously', () => {
expect(() => {
runTask(() => {
queueMicrotask(() => {
throw new Error('test error');
});
});
}).toThrow('test error');
});
it('should run async tasks synchronously', () => {
let completed = false;
runTask(async () => {
await Promise.resolve(6);
completed = true;
});
expect(completed).toBe(true);
});
});
describe('getRenderedOutput', () => {
describe('toJSX', () => {
it('default config', () => {
const root = createRoot();
runTask(() => {
root.render(
<View style={{width: 100, height: 100}} collapsable={false} />,
);
});
expect(root.getRenderedOutput().toJSX()).toEqual(
<rn-view height="100.000000" width="100.000000" />,
);
root.destroy();
});
it('default config, list of children', () => {
const root = createRoot();
runTask(() => {
root.render(
<>
<View style={{width: 100, height: 100}} collapsable={false} />
<View style={{width: 100, height: 100}} collapsable={false} />
</>,
);
});
expect(root.getRenderedOutput().toJSX()).toEqual(
<>
<rn-view width="100.000000" height="100.000000" />
<rn-view width="100.000000" height="100.000000" />
</>,
);
root.destroy();
});
it('include root', () => {
const root = createRoot();
runTask(() => {
root.render(
<View style={{width: 100, height: 100}} collapsable={false} />,
);
});
expect(root.getRenderedOutput({includeRoot: true}).toJSX()).toEqual(
<rn-rootView>
<rn-view width="100.000000" height="100.000000" />
</rn-rootView>,
);
root.destroy();
});
it('include layout metrics', () => {
const root = createRoot();
runTask(() => {
root.render(
<View style={{width: 100, height: 100}} collapsable={false} />,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
height="100.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-frame="{x:0,y:0,width:100,height:100}"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="1"
width="100.000000"
/>,
);
root.destroy();
});
it('take props', () => {
const root = createRoot();
runTask(() => {
root.render(
<View style={{width: 100, height: 100}} collapsable={false} />,
);
});
expect(
root
.getRenderedOutput({
props: ['width'],
})
.toJSX(),
).toEqual(<rn-view width="100.000000" />);
root.destroy();
});
it('skip props', () => {
const root = createRoot();
runTask(() => {
root.render(
<View style={{width: 100, height: 100}} collapsable={false} />,
);
});
expect(
root
.getRenderedOutput({
props: ['!width'],
})
.toJSX(),
).toEqual(<rn-view height="100.000000" />);
root.destroy();
});
it('filter out all props', () => {
const root = createRoot();
runTask(() => {
root.render(
<>
<View style={{width: 100, height: 100}} collapsable={false} />
<Text>hello world!</Text>
<View style={{width: 200, height: 300}} collapsable={false} />
</>,
);
});
expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(
<>
<rn-view />
<rn-paragraph>hello world!</rn-paragraph>
<rn-view />
</>,
);
root.destroy();
});
});
describe('toJSON', () => {
it('nested text', () => {
const root = createRoot();
runTask(() => {
root.render(
<Text>
Testing native{' '}
<Text style={{color: 'red'}}>
JSX is <Text style={{color: 'blue'}}>easy!</Text>
</Text>
</Text>,
);
});
expect(
root.getRenderedOutput({props: ['foreground*']}).toJSON(),
).toEqual({
children: [
'Testing native ',
{
children: 'JSX is ',
props: {
foregroundColor: 'rgba(255, 0, 0, 255)',
},
type: 'Text',
},
{
children: 'easy!',
props: {
foregroundColor: 'rgba(0, 0, 255, 255)',
},
type: 'Text',
},
],
props: {
foregroundColor: 'rgba(255, 255, 255, 127)',
},
type: 'Paragraph',
});
root.destroy();
});
});
});
});
@@ -0,0 +1,23 @@
/**
* 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 commonTestFlag:true jsOnlyTestFlag:true
*/
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
describe('FantomFeatureFlags', () => {
it('allows overridding common feature flags', () => {
expect(ReactNativeFeatureFlags.commonTestFlag()).toBe(true);
});
it('allows overridding JS-only feature flags', () => {
expect(ReactNativeFeatureFlags.jsOnlyTestFlag()).toBe(true);
});
});
@@ -4,11 +4,13 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @flow strict-local
* @format
* @oncall react_native
*/
export {render} from './renderer/index.js';
export {ReactNativeEnvironment} from './jest/environment.js';
describe('no "@fantom_mode" in docblock', () => {
it('should use development builds', () => {
expect(__DEV__).toBe(true);
});
});
@@ -7,10 +7,11 @@
* @flow strict-local
* @format
* @oncall react_native
* @fantom_mode dev
*/
import * as ReactNativeFeatureFlags from '../../../../src/private/featureflags/ReactNativeFeatureFlags';
ReactNativeFeatureFlags.override({
enableAccessToHostTreeInFabric: () => true,
describe('"@fantom_mode dev" in docblock', () => {
it('should use development builds', () => {
expect(__DEV__).toBe(true);
});
});
@@ -0,0 +1,17 @@
/**
* 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_mode dev-bytecode
*/
describe('"@fantom_mode dev-bytecode" in docblock', () => {
it('should use development builds', () => {
expect(__DEV__).toBe(true);
});
});
@@ -6,12 +6,12 @@
*
* @flow strict-local
* @format
* @oncall react_native
* @fantom_mode opt
*/
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-flow',
],
plugins: ['@babel/plugin-transform-react-jsx'],
};
describe('"@fantom_mode opt" in docblock', () => {
it('should use optimized builds', () => {
expect(__DEV__).toBe(false);
});
});
@@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`expect toMatchSnapshot() complex types 1`] = `
Object {
"foo": "bar",
}
`;
exports[`expect toMatchSnapshot() complex types 2`] = `
<span>
hello
</span>
`;
exports[`expect toMatchSnapshot() complex types 3`] = `[Function foo]`;
exports[`expect toMatchSnapshot() complex types 4`] = `
Map {
"foo" => "bar",
}
`;
exports[`expect toMatchSnapshot() complex types 5`] = `
Set {
1,
2,
}
`;
exports[`expect toMatchSnapshot() complex types 6`] = `2025-01-02T00:00:00.000Z`;
exports[`expect toMatchSnapshot() complex types 7`] = `[Error]`;
exports[`expect toMatchSnapshot() complex types 8`] = `/asd/`;
exports[`expect toMatchSnapshot() complex types 9`] = `
Promise {
"_h": 0,
"_i": 0,
"_j": null,
"_k": null,
}
`;
exports[`expect toMatchSnapshot() named snapshots: named snapshot 1`] = `
Object {
"a": "b",
}
`;
exports[`expect toMatchSnapshot() primitive types 1`] = `undefined`;
exports[`expect toMatchSnapshot() primitive types 2`] = `null`;
exports[`expect toMatchSnapshot() primitive types 3`] = `true`;
exports[`expect toMatchSnapshot() primitive types 4`] = `1`;
exports[`expect toMatchSnapshot() primitive types 5`] = `1n`;
exports[`expect toMatchSnapshot() primitive types 6`] = `"foo"`;
exports[`expect toMatchSnapshot() primitive types 8`] = `Symbol(foo)`;
exports[`expect toMatchSnapshot() primitive types: multiline 7`] = `
"foo
bar"
`;
@@ -0,0 +1,546 @@
/**
* 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 React from 'react';
function ensureError(fn: () => void): void {
try {
fn();
} catch (e) {
return;
}
throw new Error(`Expected function to throw, but it didn't`);
}
describe('expect', () => {
test('toThrow', () => {
expect(() => {
throw new Error();
}).toThrow();
expect(() => {
throw new Error('error message');
}).toThrow('error message');
expect(() => {
throw new Error('error message');
}).not.toThrow('error message 2');
expect(() => {}).not.toThrow();
ensureError(() => {
expect(() => {}).toThrow();
});
ensureError(() => {
expect(() => {
throw new Error();
}).not.toThrow();
});
});
test('toBe', () => {
expect(1).toBe(1);
expect(1).not.toBe(2);
const obj = {a: 1};
const obj2 = {a: 1};
expect(obj).toBe(obj);
expect(obj).not.toBe(obj2);
expect(() => {
expect(obj).not.toBe(obj);
}).toThrow();
expect(() => {
expect(1).not.toBe(1);
}).toThrow();
});
test('toEqual', () => {
expect(1).toEqual(1);
expect(1).not.toEqual(2);
const obj = {a: 1};
const obj2 = {a: 1};
const obj3 = {a: 2};
expect(obj).toEqual(obj);
expect(obj).toEqual(obj2);
expect(obj).not.toEqual(obj3);
expect(null).toEqual(null);
expect(undefined).toEqual(undefined);
expect(null).not.toEqual(undefined);
expect({a: null}).not.toEqual({a: undefined});
expect({a: undefined}).not.toEqual({});
expect(() => {
expect(obj).not.toEqual(obj2);
}).toThrow();
expect(() => {
expect(obj).toEqual(obj3);
}).toThrow();
expect(() => {
expect(1).not.toEqual(1);
}).toThrow();
expect(() => {
expect(null).not.toEqual(null);
}).toThrow();
expect(() => {
expect(undefined).not.toEqual(undefined);
}).toThrow();
expect(() => {
expect({a: undefined}).toEqual({});
}).toThrow();
});
test('toBeInstanceOf', () => {
class Class {}
expect(1).not.toBeInstanceOf(Number);
expect(1).not.toBeInstanceOf(Class);
expect(new Class()).toBeInstanceOf(Class);
expect(new Class()).toBeInstanceOf(Object);
expect(new Class()).not.toBeInstanceOf(Number);
expect(() => {
expect(1).toBeInstanceOf(Number);
}).toThrow();
expect(() => {
expect(new Class()).not.toBeInstanceOf(Class);
}).toThrow();
});
test('toBeCloseTo', () => {
expect(1).toBeCloseTo(1.001);
expect(1).toBeCloseTo(1.01, 1);
expect(1).toBeCloseTo(1.1, 0);
expect(() => {
expect(1).toBeCloseTo(1.01);
}).toThrow();
expect(() => {
expect(1).toBeCloseTo(1.1, 1);
}).toThrow();
expect(() => {
expect(1).toBeCloseTo(2, 0);
}).toThrow();
});
test('toHaveBeenCalled', () => {
const fn = jest.fn();
expect(fn).not.toHaveBeenCalled();
expect(() => {
expect(fn).toHaveBeenCalled();
}).toThrow();
fn();
expect(fn).toHaveBeenCalled();
expect(() => {
expect(fn).not.toHaveBeenCalled();
}).toThrow();
// Passing functions that aren't mocks should always fail
expect(() => {
expect(() => {}).toHaveBeenCalled();
}).toThrow();
expect(() => {
expect(() => {}).not.toHaveBeenCalled();
}).toThrow();
});
test('toHaveBeenCalledTimes', () => {
const fn = jest.fn();
expect(fn).toHaveBeenCalledTimes(0);
expect(fn).not.toHaveBeenCalledTimes(1);
expect(() => {
expect(fn).not.toHaveBeenCalledTimes(0);
}).toThrow();
expect(() => {
expect(fn).toHaveBeenCalledTimes(1);
}).toThrow();
fn();
expect(fn).not.toHaveBeenCalledTimes(0);
expect(fn).toHaveBeenCalledTimes(1);
expect(() => {
expect(fn).toHaveBeenCalledTimes(0);
}).toThrow();
expect(() => {
expect(fn).not.toHaveBeenCalledTimes(1);
}).toThrow();
// Passing functions that aren't mocks should always fail
expect(() => {
expect(() => {}).toHaveBeenCalledTimes(0);
}).toThrow();
expect(() => {
expect(() => {}).not.toHaveBeenCalledTimes(1);
}).toThrow();
});
describe('jest.fn()', () => {
it('tracks execution of functions without implementations', () => {
const fn = jest.fn();
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
expect(fn()).toBe(undefined);
expect(fn.mock.calls).toEqual([[]]);
expect(fn.mock.lastCall).toEqual([]);
expect(fn.mock.instances).toEqual([undefined]);
expect(fn.mock.contexts).toEqual([global]);
expect(fn.mock.contexts[0]).toBe(global);
expect(fn.mock.results).toEqual([{value: undefined, isThrow: false}]);
});
it('tracks execution of methods without implementations', () => {
const fn = jest.fn();
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
const obj = {fn};
expect(obj.fn()).toBe(undefined);
expect(fn.mock.calls).toEqual([[]]);
expect(fn.mock.lastCall).toEqual([]);
expect(fn.mock.instances).toEqual([undefined]);
expect(fn.mock.contexts).toEqual([obj]);
expect(fn.mock.contexts[0]).toBe(obj);
expect(fn.mock.results).toEqual([{value: undefined, isThrow: false}]);
});
it('tracks constructors without implementations', () => {
const fn = jest.fn();
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
// $FlowExpectedError[invalid-constructor]
const instance = new fn();
expect(instance).toBeInstanceOf(Object);
expect(fn.mock.calls).toEqual([[]]);
expect(fn.mock.lastCall).toEqual([]);
expect(fn.mock.instances).toEqual([instance]);
expect(fn.mock.instances[0]).toBe(instance);
expect(fn.mock.contexts).toEqual([instance]);
expect(fn.mock.contexts[0]).toBe(instance);
expect(fn.mock.results).toEqual([{value: undefined, isThrow: false}]);
});
it('tracks execution of functions with an implementation', () => {
const fn = jest.fn((a, b) => {
return a + b;
});
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
expect(fn(1, 2)).toBe(3);
expect(fn.mock.calls).toEqual([[1, 2]]);
expect(fn.mock.lastCall).toEqual([1, 2]);
expect(fn.mock.instances).toEqual([undefined]);
expect(fn.mock.contexts).toEqual([global]);
expect(fn.mock.contexts[0]).toBe(global);
expect(fn.mock.results).toEqual([{value: 3, isThrow: false}]);
});
it('tracks execution of methods with an implementation', () => {
const fn = jest.fn(function (this: {prop: number}): number {
return this.prop;
});
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
const obj = {fn, prop: 2};
expect(obj.fn()).toBe(2);
expect(fn.mock.calls).toEqual([[]]);
expect(fn.mock.lastCall).toEqual([]);
expect(fn.mock.instances).toEqual([undefined]);
expect(fn.mock.contexts).toEqual([obj]);
expect(fn.mock.contexts[0]).toBe(obj);
expect(fn.mock.results).toEqual([{value: 2, isThrow: false}]);
});
it('tracks constructors with an implementation', () => {
const fn = jest.fn(function (this: {prop: number}) {
this.prop = 3;
});
expect(fn).toBeInstanceOf(Function);
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.lastCall).toBe(undefined);
expect(fn.mock.instances).toEqual([]);
expect(fn.mock.contexts).toEqual([]);
expect(fn.mock.results).toEqual([]);
// $FlowExpectedError[invalid-constructor]
const instance = new fn();
expect(instance).toBeInstanceOf(Object);
expect(instance.prop).toBe(3);
expect(fn.mock.calls).toEqual([[]]);
expect(fn.mock.lastCall).toEqual([]);
expect(fn.mock.instances).toEqual([instance]);
expect(fn.mock.instances[0]).toBe(instance);
expect(fn.mock.contexts).toEqual([instance]);
expect(fn.mock.contexts[0]).toBe(instance);
expect(fn.mock.results).toEqual([{value: undefined, isThrow: false}]);
});
});
test('toBeNull()', () => {
expect(null).toBeNull();
expect('string value').not.toBeNull();
expect(() => {
expect(null).not.toBeNull();
}).toThrow();
expect(() => {
expect('string value').toBeNull();
}).toThrow();
});
test('toBeLessThan', () => {
expect(1).toBeLessThan(2);
expect(1).not.toBeLessThan(1);
expect(1).not.toBeLessThan(0);
expect(() => {
expect(1).toBeLessThan(0);
}).toThrow();
expect(() => {
expect(1).toBeLessThan(1);
}).toThrow();
expect(() => {
expect(1).not.toBeLessThan(2);
}).toThrow();
// Should always throw if the received value isn't a number
expect(() => {
expect('string value').toBeLessThan(1);
}).toThrow();
expect(() => {
expect('string value').not.toBeLessThan(1);
}).toThrow();
// Should always throw if the expected value isn't a number
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).toBeLessThan('string value');
}).toThrow();
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).not.toBeLessThan('string value');
}).toThrow();
});
test('toBeLessThanOrEqual', () => {
expect(1).toBeLessThanOrEqual(1);
expect(1).toBeLessThanOrEqual(2);
expect(1).not.toBeLessThanOrEqual(0.8);
expect(() => {
expect(1).not.toBeLessThanOrEqual(1);
}).toThrow();
expect(() => {
expect(1).not.toBeLessThanOrEqual(2);
}).toThrow();
// Should always throw if the received value isn't a number
expect(() => {
expect('string value').toBeLessThanOrEqual(1);
}).toThrow();
expect(() => {
expect('string value').not.toBeLessThanOrEqual(1);
}).toThrow();
// Should always throw if the expected value isn't a number
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).toBeLessThanOrEqual('string value');
}).toThrow();
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).not.toBeLessThanOrEqual('string value');
}).toThrow();
});
test('toBeGreaterThan', () => {
expect(1).toBeGreaterThan(0);
expect(1).not.toBeGreaterThan(1);
expect(1).not.toBeGreaterThan(2);
expect(() => {
expect(1).toBeGreaterThan(2);
}).toThrow();
expect(() => {
expect(1).not.toBeGreaterThan(0);
}).toThrow();
// Should always throw if the received value isn't a number
expect(() => {
expect('string value').toBeGreaterThan(1);
}).toThrow();
expect(() => {
expect('string value').not.toBeGreaterThan(1);
}).toThrow();
// Should always throw if the expected value isn't a number
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).toBeGreaterThan('string value');
}).toThrow();
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).not.toBeGreaterThan('string value');
}).toThrow();
});
test('toBeGreaterThanOrEqual', () => {
expect(1).toBeGreaterThanOrEqual(0);
expect(1).toBeGreaterThanOrEqual(1);
expect(1).not.toBeGreaterThanOrEqual(2);
expect(() => {
expect(1).not.toBeGreaterThanOrEqual(0);
}).toThrow();
expect(() => {
expect(1).not.toBeGreaterThanOrEqual(1);
}).toThrow();
// Should always throw if the received value isn't a number
expect(() => {
expect('string value').toBeGreaterThanOrEqual(1);
}).toThrow();
expect(() => {
expect('string value').not.toBeGreaterThanOrEqual(1);
}).toThrow();
// Should always throw if the expected value isn't a number
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).toBeGreaterThanOrEqual('string value');
}).toThrow();
expect(() => {
// $FlowExpectedError[incompatible-call]
expect(1).not.toBeGreaterThanOrEqual('string value');
}).toThrow();
});
describe('toMatchSnapshot()', () => {
test('primitive types', () => {
expect(undefined).toMatchSnapshot();
expect(null).toMatchSnapshot();
expect(true).toMatchSnapshot();
expect(1).toMatchSnapshot();
expect(BigInt(1)).toMatchSnapshot();
expect('foo').toMatchSnapshot();
expect('foo\nbar').toMatchSnapshot('multiline');
expect(Symbol('foo')).toMatchSnapshot();
});
test('complex types', () => {
expect({foo: 'bar'}).toMatchSnapshot();
expect(<span>hello</span>).toMatchSnapshot();
expect(function foo() {}).toMatchSnapshot();
expect(new Map([['foo', 'bar']])).toMatchSnapshot();
expect(new Set([1, 2])).toMatchSnapshot();
expect(new Date('2025-01-02')).toMatchSnapshot();
expect(new Error()).toMatchSnapshot();
expect(new RegExp('asd')).toMatchSnapshot();
expect(new Promise(() => {})).toMatchSnapshot();
});
test('named snapshots', () => {
expect({a: 'b'}).toMatchSnapshot('named snapshot');
});
});
});
@@ -0,0 +1,208 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
// $FlowExpectedError[untyped-import]
import micromatch from 'micromatch';
import * as React from 'react';
export type RenderOutputConfig = {
...FantomRenderedOutputConfig,
includeRoot?: boolean,
includeLayoutMetrics?: boolean,
};
// match RenderFormatOptions.h
type NativeRenderFormatOptions = {
includeRoot: boolean,
includeLayoutMetrics: boolean,
};
type FantomJsonObject = {
type: string,
props: {[key: string]: string},
children: $ReadOnlyArray<FantomJsonObject | string>,
};
type FantomJson = FantomJsonObject | $ReadOnlyArray<FantomJsonObject>;
type FantomRenderedOutputConfig = {
// micromatch pattern to match prop names
// see usage examples in https://github.com/micromatch/micromatch#examples
props?: $ReadOnlyArray<string>,
};
class FantomRenderedOutput {
#json: FantomJson;
constructor(json: FantomJson, config: FantomRenderedOutputConfig) {
this.#json = this.#filterJson(json, config);
}
toJSON(): FantomJson {
return Array.isArray(this.#json) ? [...this.#json] : {...this.#json};
}
toJSX(): React.Node {
return convertRawJsonToJSX(this.#json);
}
#filterJson(
json: FantomJson,
config: FantomRenderedOutputConfig,
): FantomJson {
if (Array.isArray(json)) {
return json.map(child => this.#filterJsonObject(child, config));
} else {
return this.#filterJsonObject(json, config);
}
}
#filterJsonObject(
json: FantomJsonObject,
config: FantomRenderedOutputConfig,
): FantomJsonObject {
const root: FantomJsonObject = {
type: json.type,
props: this.#filterProps(json.props, config),
children: [],
};
if (Array.isArray(json.children)) {
root.children = json.children.map(child =>
typeof child === 'object'
? this.#filterJsonObject(child, config)
: child,
);
} else {
root.children = json.children;
}
return root;
}
#filterProps(
props: FantomJsonObject['props'],
config: FantomRenderedOutputConfig,
): FantomJsonObject['props'] {
if (config.props == null) {
return {...props};
}
return micromatch(Object.keys(props), config.props ?? []).reduce(
(acc, name) => {
acc[name] = props[name];
return acc;
},
{},
);
}
}
export type {FantomRenderedOutput};
export default function getFantomRenderedOutput(
surfaceId: number,
config: RenderOutputConfig,
): FantomRenderedOutput {
const {
includeRoot = false,
includeLayoutMetrics = false,
...fantomConfig
} = config;
const nativeConfig: NativeRenderFormatOptions = {
includeRoot,
includeLayoutMetrics,
};
return new FantomRenderedOutput(
JSON.parse(
global.$$JSTesterModuleName$$.getRenderedOutput(surfaceId, nativeConfig),
),
fantomConfig,
);
}
function convertRawJsonToJSX(
actualJSON: FantomJsonObject | $ReadOnlyArray<FantomJsonObject>,
): React.Node {
let actualJSX;
if (actualJSON === null || typeof actualJSON === 'string') {
actualJSX = actualJSON;
} else if (Array.isArray(actualJSON)) {
if (actualJSON.length === 0) {
actualJSX = null;
} else if (actualJSON.length === 1) {
actualJSX = jsonChildToJSXChild(actualJSON[0]);
} else {
const actualJSXChildren = jsonChildrenToJSXChildren(actualJSON);
if (actualJSXChildren === null || typeof actualJSXChildren === 'string') {
actualJSX = actualJSXChildren;
} else {
actualJSX = <>{actualJSXChildren}</>;
}
}
} else {
actualJSX = jsonChildToJSXChild(actualJSON);
}
return actualJSX;
}
function createJSXElementForTestComparison(
type: string,
props: mixed,
): React.Node {
const Tag = type;
return <Tag {...props} />;
}
function rnTypeToTestType(type: string): string {
return `rn-${type.substring(0, 1).toLowerCase() + type.substring(1)}`;
}
function jsonChildToJSXChild(jsonChild: FantomJsonObject | string): React.Node {
if (typeof jsonChild === 'string') {
return jsonChild;
} else {
const jsxChildren = jsonChildrenToJSXChildren(jsonChild.children);
const type = rnTypeToTestType(jsonChild.type);
return createJSXElementForTestComparison(
type,
jsxChildren == null
? jsonChild.props
: {...jsonChild.props, children: jsxChildren},
);
}
}
function jsonChildrenToJSXChildren(jsonChildren: FantomJsonObject['children']) {
if (jsonChildren.length === 1) {
return jsonChildToJSXChild(jsonChildren[0]);
} else if (jsonChildren.length > 1) {
const jsxChildren = [];
let allJSXChildrenAreStrings = true;
let jsxChildrenString = '';
for (let i = 0; i < jsonChildren.length; i++) {
const jsxChild = jsonChildToJSXChild(jsonChildren[i]);
jsxChildren.push(jsxChild);
if (allJSXChildrenAreStrings) {
if (typeof jsxChild === 'string') {
jsxChildrenString += jsxChild;
} else if (jsxChild !== null) {
allJSXChildrenAreStrings = false;
}
}
}
return allJSXChildrenAreStrings ? jsxChildrenString : jsxChildren;
}
return null;
}
@@ -8,9 +8,14 @@
* @format
*/
import type {
FantomRenderedOutput,
RenderOutputConfig,
} from './getFantomRenderedOutput';
import type {MixedElement} from 'react';
import ReactFabric from '../../../Libraries/Renderer/shims/ReactFabric';
import getFantomRenderedOutput from './getFantomRenderedOutput';
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
let globalSurfaceIdCounter = 1;
@@ -48,6 +53,10 @@ class Root {
global.$$JSTesterModuleName$$.flushMessageQueue();
}
getRenderedOutput(config: RenderOutputConfig = {}): FantomRenderedOutput {
return getFantomRenderedOutput(this.#surfaceId, config);
}
// TODO: add an API to check if all surfaces were deallocated when tests are finished.
}
@@ -56,7 +65,7 @@ class Root {
*
* React must run inside of event loop to ensure scheduling environment is closer to production.
*/
export function runTask(task: () => void) {
export function runTask(task: () => void | Promise<void>) {
nativeRuntimeScheduler.unstable_scheduleCallback(
schedulerPriorityImmediate,
task,
@@ -1,25 +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.
*
* @format
*/
'use strict';
module.exports = {
haste: {
defaultPlatform: 'ios',
platforms: ['android', 'ios', 'native'],
},
transform: {
'^.+\\.(js|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)',
],
setupFilesAfterEnv: ['./src/jest/setup-files-after-env'],
testEnvironment: './src/jest/environment',
};
@@ -1,18 +0,0 @@
{
"name": "@react-native/test-renderer",
"private": true,
"version": "0.77.0-main",
"description": "A Test rendering library for React Native",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/preset-flow": "^7.20.0"
},
"dependencies": {},
"main": "src/index.js",
"peerDependencies": {
"jest": "^29.7.0"
}
}
@@ -1,74 +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.
*
* @format
*/
'use strict';
const NodeEnv = require('jest-environment-node').TestEnvironment;
module.exports = class ReactNativeEnvironment extends NodeEnv {
customExportConditions = ['require', 'react-native'];
constructor(config, context) {
super(config, context);
}
async setup() {
await super.setup();
this.assignGlobals();
this.initializeTurboModuleRegistry();
}
assignGlobals() {
Object.defineProperties(this.global, {
__DEV__: {
configurable: true,
enumerable: true,
value: true,
writable: true,
},
});
this.global.IS_REACT_ACT_ENVIRONMENT = true;
}
initializeTurboModuleRegistry() {
const dims = {width: 100, height: 100, scale: 1, fontScale: 1};
const DIMS = {
screen: {
...dims,
},
window: {
...dims,
},
};
this.global.nativeModuleProxy = name => ({})[name];
this.global.__turboModuleProxy = name =>
({
SourceCode: {getConstants: () => ({scriptURL: ''})},
WebSocketModule: {connect: () => {}},
FileReaderModule: {},
AppState: {getConstants: () => ({}), getCurrentAppState: () => ({})},
DeviceInfo: {getConstants: () => ({Dimensions: DIMS})},
UIManager: {getConstants: () => ({})},
Timing: {},
DevSettings: {},
PlatformConstants: {
getConstants: () => ({reactNativeVersion: '1000.0.0'}),
},
Networking: {},
ImageLoader: {},
NativePerformanceCxx: {},
LogBox: {},
SettingsManager: {
getConstants: () => ({settings: {}}),
},
LinkingManager: {},
I18n: {getConstants: () => ({})},
})[name];
}
};
@@ -1,222 +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.
*
* @format
*/
'use strict';
jest.requireActual('@react-native/js-polyfills/error-guard');
jest
.mock('react-native/Libraries/ReactNative/UIManager', () => ({
AndroidViewPager: {
Commands: {
setPage: jest.fn(),
setPageWithoutAnimation: jest.fn(),
},
},
blur: jest.fn(),
createView: jest.fn(),
customBubblingEventTypes: {},
customDirectEventTypes: {},
getConstants: () => ({
ViewManagerNames: [],
}),
getDefaultEventTypes: jest.fn(),
dispatchViewManagerCommand: jest.fn(),
focus: jest.fn(),
getViewManagerConfig: jest.fn(name => {
if (name === 'AndroidDrawerLayout') {
return {
Constants: {
DrawerPosition: {
Left: 10,
},
},
};
}
return {NativeProps: {}};
}),
hasViewManagerConfig: jest.fn(name => {
return name === 'AndroidDrawerLayout';
}),
measure: jest.fn(),
manageChildren: jest.fn(),
removeSubviewsFromContainerWithID: jest.fn(),
replaceExistingNonRootView: jest.fn(),
setChildren: jest.fn(),
updateView: jest.fn(),
AndroidDrawerLayout: {
Constants: {
DrawerPosition: {
Left: 10,
},
},
},
AndroidTextInput: {
Commands: {},
},
ScrollView: {
Constants: {},
},
View: {
Constants: {},
},
}))
// Mock modules defined by the native layer (ex: Objective-C, Java)
.mock('react-native/Libraries/BatchedBridge/NativeModules', () => ({
AlertManager: {
alertWithArgs: jest.fn(),
},
AsyncLocalStorage: {
multiGet: jest.fn((keys, callback) =>
process.nextTick(() => callback(null, [])),
),
multiSet: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
multiRemove: jest.fn((keys, callback) =>
process.nextTick(() => callback(null)),
),
multiMerge: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
clear: jest.fn(callback => process.nextTick(() => callback(null))),
getAllKeys: jest.fn(callback =>
process.nextTick(() => callback(null, [])),
),
},
DeviceInfo: {
getConstants() {
return {
Dimensions: {
window: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
screen: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
},
};
},
},
DevSettings: {
addMenuItem: jest.fn(),
reload: jest.fn(),
},
ImageLoader: {
getSize: jest.fn(url => Promise.resolve([320, 240])),
prefetchImage: jest.fn(),
},
ImageViewManager: {
getSize: jest.fn((uri, success) =>
process.nextTick(() => success(320, 240)),
),
prefetchImage: jest.fn(),
},
KeyboardObserver: {
addListener: jest.fn(),
removeListeners: jest.fn(),
},
Networking: {
sendRequest: jest.fn(),
abortRequest: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
PlatformConstants: {
getConstants() {
return {
reactNativeVersion: {
major: 1000,
minor: 0,
patch: 0,
},
};
},
},
PushNotificationManager: {
presentLocalNotification: jest.fn(),
scheduleLocalNotification: jest.fn(),
cancelAllLocalNotifications: jest.fn(),
removeAllDeliveredNotifications: jest.fn(),
getDeliveredNotifications: jest.fn(callback =>
process.nextTick(() => []),
),
removeDeliveredNotifications: jest.fn(),
setApplicationIconBadgeNumber: jest.fn(),
getApplicationIconBadgeNumber: jest.fn(callback =>
process.nextTick(() => callback(0)),
),
cancelLocalNotifications: jest.fn(),
getScheduledLocalNotifications: jest.fn(callback =>
process.nextTick(() => callback()),
),
requestPermissions: jest.fn(() =>
Promise.resolve({alert: true, badge: true, sound: true}),
),
abandonPermissions: jest.fn(),
checkPermissions: jest.fn(callback =>
process.nextTick(() =>
callback({alert: true, badge: true, sound: true}),
),
),
getInitialNotification: jest.fn(() => Promise.resolve(null)),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
StatusBarManager: {
setColor: jest.fn(),
setStyle: jest.fn(),
setHidden: jest.fn(),
setNetworkActivityIndicatorVisible: jest.fn(),
setBackgroundColor: jest.fn(),
setTranslucent: jest.fn(),
getConstants: () => ({
HEIGHT: 42,
}),
},
Timing: {
createTimer: jest.fn(),
deleteTimer: jest.fn(),
},
UIManager: {},
BlobModule: {
getConstants: () => ({BLOB_URI_SCHEME: 'content', BLOB_URI_HOST: null}),
addNetworkingHandler: jest.fn(),
enableBlobSupport: jest.fn(),
disableBlobSupport: jest.fn(),
createFromParts: jest.fn(),
sendBlob: jest.fn(),
release: jest.fn(),
},
WebSocketModule: {
connect: jest.fn(),
send: jest.fn(),
sendBinary: jest.fn(),
ping: jest.fn(),
close: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
I18nManager: {
allowRTL: jest.fn(),
forceRTL: jest.fn(),
swapLeftAndRightInRTL: jest.fn(),
getConstants: () => ({
isRTL: false,
doLeftAndRightSwapInRTL: true,
}),
},
}));
@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render toJSON renders View props 1`] = `
<RCTView
pointerEvents="box-none"
>
<RCTText
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
isHighlighted={false}
selectionColor={null}
>
Hello
</RCTText>
<RCTView
style={
{
"flex": 1,
}
}
/>
</RCTView>
`;
exports[`render toJSON returns expected JSON output based on renderer component 1`] = `
<RCTView>
<RCTText
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
isHighlighted={false}
selectionColor={null}
>
Hello
</RCTText>
<RCTView />
</RCTView>
`;
@@ -1,63 +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.
*
* @format
* @flow
*/
'use strict';
import * as ReactNativeTestRenderer from '../index';
import * as React from 'react';
import {Text, View} from 'react-native';
import 'react-native/Libraries/Components/View/ViewNativeComponent';
function TestComponent() {
return (
<View>
<Text>Hello</Text>
<View />
</View>
);
}
function TestComponentWithProps() {
return (
<View pointerEvents="box-none">
<Text>Hello</Text>
<View style={{flex: 1}} />
</View>
);
}
describe('render', () => {
describe('toJSON', () => {
it('returns expected JSON output based on renderer component', () => {
const result = ReactNativeTestRenderer.render(<TestComponent />);
expect(result.toJSON()).toMatchSnapshot();
});
it('renders View props', () => {
const result = ReactNativeTestRenderer.render(<TestComponentWithProps />);
expect(result.toJSON()).toMatchSnapshot();
});
});
describe('findAll', () => {
it('returns all nodes matching the predicate', () => {
const result = ReactNativeTestRenderer.render(<TestComponent />);
const textNode = result.findAll(node => {
return node.props?.text === 'Hello';
})[0];
expect(textNode).not.toBeUndefined();
const viewNodes = result.findAll(node => {
return node.viewName === 'RCTView';
});
expect(viewNodes.length).toBe(2);
});
});
});
@@ -1,114 +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.
*
* @format
* @flow
*/
'use strict';
import * as FabricUIManager from 'react-native/Libraries/ReactNative/__mocks__/FabricUIManager';
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
import {act} from 'react-test-renderer';
type FiberPartial = {
pendingProps: {
children: $ReadOnlyArray<ReactNode>,
...
},
...
};
type ReactNode = {
children: ?Array<ReactNode>,
props: {text?: string | null, ...},
viewName: string,
instanceHandle: FiberPartial,
};
type RenderedNodeJSON = {
type: string,
props: {[propName: string]: any, ...},
children: null | Array<RenderedJSON>,
$$typeof?: symbol, // Optional because we add it with defineProperty().
};
type RenderedJSON = RenderedNodeJSON | string;
type RenderResult = {
toJSON: () => Array<RenderedJSON> | RenderedJSON | null,
findAll: (predicate: (ReactNode) => boolean) => Array<ReactNode>,
};
function buildRenderResult(rootNode: ReactNode): RenderResult {
return {
toJSON: () => toJSON(rootNode),
findAll: (predicate: ReactNode => boolean) => findAll(rootNode, predicate),
};
}
export function render(element: React.MixedElement): RenderResult {
const manager = FabricUIManager.getFabricUIManager();
if (!manager) {
throw new Error('No FabricUIManager found');
}
const containerTag = Math.round(Math.random() * 1000000);
act(() => {
ReactFabric.render(element, containerTag, () => {}, true);
});
// $FlowFixMe
const root: [ReactNode] = manager.getRoot(containerTag);
if (root == null) {
throw new Error('No root found for containerTag ' + containerTag);
}
return buildRenderResult(root[0]);
}
function toJSON(node: ReactNode): RenderedJSON {
let renderedChildren = null;
if (node.children != null && node.children.length > 0) {
renderedChildren = node.children.map(c => toJSON(c));
}
if (node.viewName === 'RCTRawText') {
return node.props.text ?? '';
}
const {children: _children, ...props} =
node.instanceHandle?.pendingProps ?? {};
const json: RenderedNodeJSON = {
type: node.viewName,
props,
children: renderedChildren,
};
Object.defineProperty(json, '$$typeof', {
value: Symbol.for('react.test.json'),
});
return json;
}
function findAll(
node: ReactNode,
predicate: ReactNode => boolean,
): Array<ReactNode> {
const results = [];
if (predicate(node)) {
results.push(node);
}
if (node.children != null && node.children.length > 0) {
for (const child of node.children) {
results.push(...findAll(child, predicate));
}
}
return results;
}
@@ -121,12 +121,10 @@ describe('Animated', () => {
await unmount(root);
expect(callback).not.toBeCalled();
await jest.runOnlyPendingTimersAsync();
expect(callback).toBeCalledWith({finished: false});
});
it('triggers callback when spring is at rest', async () => {
it('triggers callback when spring is at rest', () => {
const anim = new Animated.Value(0);
const callback = jest.fn();
Animated.spring(anim, {
@@ -134,10 +132,7 @@ describe('Animated', () => {
velocity: 0,
useNativeDriver: false,
}).start(callback);
expect(callback).not.toBeCalled();
await jest.runOnlyPendingTimersAsync();
expect(callback).toBeCalledWith({finished: true});
expect(callback).toBeCalled();
});
it('send toValue when a critically damped spring stops', () => {
@@ -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(
@@ -74,7 +79,16 @@ export default class Animation {
stop(): void {
if (this.#nativeID != null) {
NativeAnimatedHelper.API.stopAnimation(this.#nativeID);
const nativeID = this.#nativeID;
const identifier = `${nativeID}:stopAnimation`;
try {
// This is only required when singleOpBatching is used, as otherwise
// we flush calls immediately when there's no pending queue.
NativeAnimatedHelper.API.setWaitingForIdentifier(identifier);
NativeAnimatedHelper.API.stopAnimation(nativeID);
} finally {
NativeAnimatedHelper.API.unsetWaitingForIdentifier(identifier);
}
}
this.__active = false;
}
@@ -165,7 +179,14 @@ export default class Animation {
const callback = this.#onEnd;
if (callback != null) {
this.#onEnd = null;
queueMicrotask(() => callback(result));
callback(result);
}
}
__getDebugID(): ?string {
if (__DEV__) {
return this.__debugID;
}
return undefined;
}
}
@@ -66,6 +66,7 @@ export default class DecayAnimation extends Animation {
velocity: this._velocity,
iterations: this.__iterations,
platformConfig: this._platformConfig,
debugID: this.__getDebugID(),
};
}
@@ -194,6 +194,7 @@ export default class SpringAnimation extends Animation {
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
debugID: this.__getDebugID(),
};
}
@@ -99,6 +99,7 @@ export default class TimingAnimation extends Animation {
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
debugID: this.__getDebugID(),
};
}
@@ -13,6 +13,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
@@ -22,8 +23,12 @@ export default class AnimatedAddition extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
@@ -59,6 +64,7 @@ export default class AnimatedAddition extends AnimatedWithChildren {
return {
type: 'addition',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}
@@ -14,6 +14,7 @@ import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {NativeColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import normalizeColor from '../../StyleSheet/normalizeColor';
@@ -22,6 +23,7 @@ import AnimatedValue, {flushValue} from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedColorConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
@@ -118,7 +120,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
_suspendCallbacks: number = 0;
constructor(valueIn?: InputValue, config?: ?AnimatedColorConfig) {
super();
super(config);
let value: RgbaValue | RgbaAnimatedValue | ColorValue =
valueIn ?? defaultColor;
@@ -315,6 +317,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
b: this.b.__getNativeTag(),
a: this.a.__getNativeTag(),
nativeColor: this.nativeColor,
debugID: this.__getDebugID(),
};
}
}
@@ -13,6 +13,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
@@ -24,8 +25,13 @@ export default class AnimatedDiffClamp extends AnimatedWithChildren {
_value: number;
_lastValue: number;
constructor(a: AnimatedNode, min: number, max: number) {
super();
constructor(
a: AnimatedNode,
min: number,
max: number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = a;
this._min = min;
@@ -67,6 +73,7 @@ export default class AnimatedDiffClamp extends AnimatedWithChildren {
input: this._a.__getNativeTag(),
min: this._min,
max: this._max,
debugID: this.__getDebugID(),
};
}
}
@@ -12,6 +12,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedNode from './AnimatedNode';
@@ -23,8 +24,12 @@ export default class AnimatedDivision extends AnimatedWithChildren {
_b: AnimatedNode;
_warnedAboutDivideByZero: boolean = false;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) {
console.error('Detected potential division by zero in AnimatedDivision');
}
@@ -75,6 +80,7 @@ export default class AnimatedDivision extends AnimatedWithChildren {
return {
type: 'division',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}
@@ -14,6 +14,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import {validateInterpolation} from '../../../src/private/animated/NativeAnimatedValidation';
@@ -26,6 +27,7 @@ import invariant from 'invariant';
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
export type InterpolationConfigType<OutputT: number | string> = $ReadOnly<{
...AnimatedNodeConfig,
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
easing?: (input: number) => number,
@@ -327,7 +329,7 @@ export default class AnimatedInterpolation<
_interpolation: ?(input: number) => OutputT;
constructor(parent: AnimatedNode, config: InterpolationConfigType<OutputT>) {
super();
super(config);
this._parent = parent;
this._config = config;
@@ -411,6 +413,7 @@ export default class AnimatedInterpolation<
extrapolateRight:
this._config.extrapolateRight || this._config.extrapolate || 'extend',
type: 'interpolation',
debugID: this.__getDebugID(),
};
}
}
@@ -13,6 +13,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
@@ -21,8 +22,8 @@ export default class AnimatedModulo extends AnimatedWithChildren {
_a: AnimatedNode;
_modulus: number;
constructor(a: AnimatedNode, modulus: number) {
super();
constructor(a: AnimatedNode, modulus: number, config?: ?AnimatedNodeConfig) {
super(config);
this._a = a;
this._modulus = modulus;
}
@@ -58,6 +59,7 @@ export default class AnimatedModulo extends AnimatedWithChildren {
type: 'modulus',
input: this._a.__getNativeTag(),
modulus: this._modulus,
debugID: this.__getDebugID(),
};
}
}
@@ -13,6 +13,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
@@ -22,8 +23,12 @@ export default class AnimatedMultiplication extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
@@ -58,6 +63,7 @@ export default class AnimatedMultiplication extends AnimatedWithChildren {
return {
type: 'multiplication',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}
@@ -19,6 +19,10 @@ const {startListeningToAnimatedNodeValue, stopListeningToAnimatedNodeValue} =
type ValueListenerCallback = (state: {value: number, ...}) => mixed;
export type AnimatedNodeConfig = $ReadOnly<{
debugID?: string,
}>;
let _uniqueId = 1;
let _assertNativeAnimatedModule: ?() => void = () => {
NativeAnimatedHelper.assertNativeAnimatedModule();
@@ -32,6 +36,18 @@ export default class AnimatedNode {
#updateSubscription: ?EventSubscription = null;
_platformConfig: ?PlatformConfig = undefined;
constructor(
config?: ?$ReadOnly<{
...AnimatedNodeConfig,
...
}>,
) {
if (__DEV__) {
this.__debugID = config?.debugID;
}
}
__attach(): void {}
__detach(): void {
this.removeAllListeners();
@@ -197,4 +213,13 @@ export default class AnimatedNode {
toJSON(): mixed {
return this.__getValue();
}
__debugID: ?string = undefined;
__getDebugID(): ?string {
if (__DEV__) {
return this.__debugID;
}
return undefined;
}
}
@@ -12,6 +12,7 @@
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
@@ -99,8 +100,12 @@ export default class AnimatedObject extends AnimatedWithChildren {
/**
* Should only be called by `AnimatedObject.from`.
*/
constructor(nodes: $ReadOnlyArray<AnimatedNode>, value: mixed) {
super();
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
value: mixed,
config?: ?AnimatedNodeConfig,
) {
super(config);
this.#nodes = nodes;
this._value = value;
}
@@ -157,6 +162,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
value: mapAnimatedNodes(this._value, node => {
return {nodeTag: node.__getNativeTag()};
}),
debugID: this.__getDebugID(),
};
}
}
@@ -9,6 +9,7 @@
*/
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type {AnimatedStyleAllowlist} from './AnimatedStyle';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
@@ -84,8 +85,9 @@ export default class AnimatedProps extends AnimatedNode {
inputProps: {[string]: mixed},
callback: () => void,
allowlist?: ?AnimatedPropsAllowlist,
config?: ?AnimatedNodeConfig,
) {
super();
super(config);
const [nodeKeys, nodes, props] = createAnimatedProps(inputProps, allowlist);
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
@@ -268,6 +270,7 @@ export default class AnimatedProps extends AnimatedNode {
return {
type: 'props',
props: propsConfig,
debugID: this.__getDebugID(),
};
}
}
@@ -9,6 +9,7 @@
*/
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import {validateStyles} from '../../../src/private/animated/NativeAnimatedValidation';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
@@ -112,8 +113,9 @@ export default class AnimatedStyle extends AnimatedWithChildren {
nodes: $ReadOnlyArray<AnimatedNode>,
style: {[string]: mixed},
inputStyle: any,
config?: ?AnimatedNodeConfig,
) {
super();
super(config);
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
this.#style = style;
@@ -238,6 +240,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
return {
type: 'style',
style: styleConfig,
debugID: this.__getDebugID(),
};
}
}
@@ -13,6 +13,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
@@ -22,8 +23,12 @@ export default class AnimatedSubtraction extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
@@ -59,6 +64,7 @@ export default class AnimatedSubtraction extends AnimatedWithChildren {
return {
type: 'subtraction',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}
@@ -12,6 +12,7 @@
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {EndCallback} from '../animations/Animation';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type AnimatedValue from './AnimatedValue';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
@@ -31,8 +32,9 @@ export default class AnimatedTracking extends AnimatedNode {
animationClass: any,
animationConfig: Object,
callback?: ?EndCallback,
config?: ?AnimatedNodeConfig,
) {
super();
super(config);
this._value = value;
this._parent = parent;
this._animationClass = animationClass;
@@ -95,6 +97,7 @@ export default class AnimatedTracking extends AnimatedNode {
animationConfig,
toValue: this._parent.__getNativeTag(),
value: this._value.__getNativeTag(),
debugID: this.__getDebugID(),
};
}
}
@@ -11,6 +11,7 @@
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import {validateTransform} from '../../../src/private/animated/NativeAnimatedValidation';
@@ -70,8 +71,9 @@ export default class AnimatedTransform extends AnimatedWithChildren {
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
transforms: $ReadOnlyArray<Transform<>>,
config?: ?AnimatedNodeConfig,
) {
super();
super(config);
this.#nodes = nodes;
this._transforms = transforms;
}
@@ -160,6 +162,7 @@ export default class AnimatedTransform extends AnimatedWithChildren {
return {
type: 'transform',
transforms: transformsConfig,
debugID: this.__getDebugID(),
};
}
}
@@ -13,6 +13,7 @@
import type Animation, {EndCallback} from '../animations/Animation';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type AnimatedTracking from './AnimatedTracking';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
@@ -21,6 +22,7 @@ import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedValueConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
@@ -90,7 +92,7 @@ export default class AnimatedValue extends AnimatedWithChildren {
_tracking: ?AnimatedTracking;
constructor(value: number, config?: ?AnimatedValueConfig) {
super();
super(config);
if (typeof value !== 'number') {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
@@ -298,6 +300,7 @@ export default class AnimatedValue extends AnimatedWithChildren {
type: 'value',
value: this._value,
offset: this._offset,
debugID: this.__getDebugID(),
};
}
}
@@ -11,12 +11,14 @@
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
export type AnimatedValueXYConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
type ValueXYListenerCallback = (value: {x: number, y: number, ...}) => mixed;
@@ -49,7 +51,7 @@ export default class AnimatedValueXY extends AnimatedWithChildren {
},
config?: ?AnimatedValueXYConfig,
) {
super();
super(config);
const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any`
if (typeof value.x === 'number' && typeof value.y === 'number') {
this.x = new AnimatedValue(value.x);
@@ -264,7 +264,7 @@
- (std::shared_ptr<facebook::react::JSRuntimeFactory>)createJSRuntimeFactory
{
#if USE_HERMES
return std::make_shared<facebook::react::RCTHermesInstance>(nullptr, nullptr, /* allocInOldGenBeforeTTI */ false);
return std::make_shared<facebook::react::RCTHermesInstance>(nullptr, /* allocInOldGenBeforeTTI */ false);
#else
return std::make_shared<facebook::react::RCTJscInstance>();
#endif
@@ -73,7 +73,6 @@ Pod::Spec.new do |s|
s.dependency "React-RCTNetwork"
s.dependency "React-RCTImage"
s.dependency "React-CoreModules"
s.dependency "React-nativeconfig"
s.dependency "React-RCTFBReactNativeSpec"
s.dependency "React-defaultsnativemodule"
@@ -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 TextInput from '../TextInput';
import * as Fantom from '@react-native/fantom';
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 = Fantom.createRoot();
Fantom.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 = Fantom.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
useLayoutEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} />;
}
Fantom.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 = Fantom.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
useEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} />;
}
Fantom.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`',
);
});
});
@@ -8,7 +8,9 @@
* @flow
*/
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import type {RequestBody} from './convertRequestBody';
import type {RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow';
import type {NativeResponseType} from './XMLHttpRequest';
// Do not require the native RCTNetworking module directly! Use this wrapper module instead.
@@ -35,19 +37,25 @@ function generateRequestId(): number {
return _requestId++;
}
const emitter = new NativeEventEmitter<$FlowFixMe>(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeNetworkingAndroid,
);
/**
* This class is a wrapper around the native RCTNetworking module. It adds a necessary unique
* This object is a wrapper around the native RCTNetworking module. It adds a necessary unique
* requestId to each network request that can be used to abort that request later on.
*/
// FIXME: use typed events
class RCTNetworking extends NativeEventEmitter<$FlowFixMe> {
constructor() {
super(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeNetworkingAndroid,
);
}
const RCTNetworking = {
addListener<K: $Keys<RCTNetworkingEventDefinitions>>(
eventType: K,
listener: (...$ElementType<RCTNetworkingEventDefinitions, K>) => mixed,
context?: mixed,
): EventSubscription {
// $FlowFixMe[incompatible-call]
return emitter.addListener(eventType, listener, context);
},
sendRequest(
method: string,
@@ -81,15 +89,15 @@ class RCTNetworking extends NativeEventEmitter<$FlowFixMe> {
withCredentials,
);
callback(requestId);
}
},
abortRequest(requestId: number) {
NativeNetworkingAndroid.abortRequest(requestId);
}
},
clearCookies(callback: (result: boolean) => any) {
clearCookies(callback: (result: boolean) => void) {
NativeNetworkingAndroid.clearCookies(callback);
}
}
},
};
export default (new RCTNetworking(): RCTNetworking);
export default RCTNetworking;
@@ -14,54 +14,9 @@ import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import convertRequestBody, {type RequestBody} from './convertRequestBody';
import NativeNetworkingIOS from './NativeNetworkingIOS';
import {type RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow';
import {type NativeResponseType} from './XMLHttpRequest';
type RCTNetworkingEventDefinitions = $ReadOnly<{
didSendNetworkData: [
[
number, // requestId
number, // progress
number, // total
],
],
didReceiveNetworkResponse: [
[
number, // requestId
number, // status
?{[string]: string}, // responseHeaders
?string, // responseURL
],
],
didReceiveNetworkData: [
[
number, // requestId
string, // response
],
],
didReceiveNetworkIncrementalData: [
[
number, // requestId
string, // responseText
number, // progress
number, // total
],
],
didReceiveNetworkDataProgress: [
[
number, // requestId
number, // loaded
number, // total
],
],
didCompleteNetworkResponse: [
[
number, // requestId
string, // error
boolean, // timeOutError
],
],
}>;
const RCTNetworking = {
addListener<K: $Keys<RCTNetworkingEventDefinitions>>(
eventType: K,
@@ -13,52 +13,7 @@
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import type {RequestBody} from './convertRequestBody';
import type {NativeResponseType} from './XMLHttpRequest';
type RCTNetworkingEventDefinitions = $ReadOnly<{
didSendNetworkData: [
[
number, // requestId
number, // progress
number, // total
],
],
didReceiveNetworkResponse: [
[
number, // requestId
number, // status
?{[string]: string}, // responseHeaders
?string, // responseURL
],
],
didReceiveNetworkData: [
[
number, // requestId
string, // response
],
],
didReceiveNetworkIncrementalData: [
[
number, // requestId
string, // responseText
number, // progress
number, // total
],
],
didReceiveNetworkDataProgress: [
[
number, // requestId
number, // loaded
number, // total
],
],
didCompleteNetworkResponse: [
[
number, // requestId
string, // error
boolean, // timeOutError
],
],
}>;
import type {RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow';
declare const RCTNetworking: interface {
addListener<K: $Keys<RCTNetworkingEventDefinitions>>(
@@ -0,0 +1,57 @@
/**
* 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
*/
'use strict';
export type RCTNetworkingEventDefinitions = $ReadOnly<{
didSendNetworkData: [
[
number, // requestId
number, // progress
number, // total
],
],
didReceiveNetworkResponse: [
[
number, // requestId
number, // status
?{[string]: string}, // responseHeaders
?string, // responseURL
],
],
didReceiveNetworkData: [
[
number, // requestId
string, // response
],
],
didReceiveNetworkIncrementalData: [
[
number, // requestId
string, // responseText
number, // progress
number, // total
],
],
didReceiveNetworkDataProgress: [
[
number, // requestId
number, // loaded
number, // total
],
],
didCompleteNetworkResponse: [
[
number, // requestId
string, // error
boolean, // timeOutError
],
],
}>;
@@ -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,307 +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
*/
// TODO(legacy-fake-timers): Fix these tests to work with modern timers.
jest.useFakeTimers({legacyFakeTimers: true});
import type {HostInstance} from '../../../Renderer/shims/ReactNativeTypes';
import * as React from 'react';
import {act} from 'react-test-renderer';
const TextInputState = require('../../../Components/TextInput/TextInputState');
const ReactFabric = require('../../../Renderer/shims/ReactFabric').default;
const ReactNativeViewConfigRegistry = require('../../../Renderer/shims/ReactNativeViewConfigRegistry');
const FabricUIManager = require('../../FabricUIManager');
const nullthrows = require('nullthrows');
const isWindows = process.platform === 'win32';
const itif = (condition: boolean) => {
return condition ? it : it.skip;
};
jest.mock('../../FabricUIManager', () =>
require('../../__mocks__/FabricUIManager'),
);
jest.mock('../../../../src/private/webapis/dom/nodes/specs/NativeDOM', () =>
require('../../../../src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock'),
);
/**
* Given a mocked function, get a correctly typed mock function that preserves
* the original function's type.
*/
function mockOf<TArguments: $ReadOnlyArray<mixed>, TReturn>(
fn: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn> {
if (!jest.isMockFunction(fn)) {
throw new Error(`Function ${fn.name} is not a mock function`);
}
return (fn: $FlowFixMe);
}
/**
* Renders a sequence of mock views as dictated by `keyLists`. The `keyLists`
* argument is an array of arrays which determines the number of render passes,
* how many views will be rendered in each pass, and what the keys are for each
* of the views.
*
* If an element in `keyLists` is null, the entire root will be unmounted.
*
* The return value is an array of arrays with the resulting refs from rendering
* each corresponding array of keys.
*
* If the corresponding array of keys is null, the returned element at that
* index will also be null.
*/
async function mockRenderKeys(
keyLists: Array<?Array<?string>>,
): Promise<Array<?Array<?HostInstance>>> {
const mockContainerTag = 11;
const MockView = ReactNativeViewConfigRegistry.register(
'RCTMockView',
() => ({
validAttributes: {foo: true, style: {}},
uiViewClassName: 'RCTMockView',
}),
);
const result: Array<?Array<?HostInstance>> = [];
for (let i = 0; i < keyLists.length; i++) {
const keyList = keyLists[i];
if (Array.isArray(keyList)) {
const refs: Array<?HostInstance> = keyList.map(key => undefined);
await act(() => {
ReactFabric.render(
<MockView>
{keyList.map((key, index) => (
<MockView
key={key}
ref={ref => {
refs[index] = ((ref: $FlowFixMe): ?HostInstance);
}}
/>
))}
</MockView>,
mockContainerTag,
);
});
// Clone `refs` to ignore future passes.
result.push([...refs]);
continue;
}
if (keyList == null) {
await act(() => {
// $FlowFixMe[prop-missing] This actually exists in ReactFabric
ReactFabric.stopSurface(mockContainerTag);
});
result.push(null);
continue;
}
throw new TypeError(
`Invalid 'keyLists' element of type ${typeof keyList}.`,
);
}
return result;
}
[
{enableAccessToHostTreeInFabric: false},
{enableAccessToHostTreeInFabric: true},
].forEach(flags => {
describe(`ReactFabricPublicInstance (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric = ${String(
flags.enableAccessToHostTreeInFabric,
)})'`, () => {
beforeEach(() => {
jest.resetModules();
// Installs the global `nativeFabricUIManager` pointing to the mock.
require('../../../ReactNative/__mocks__/FabricUIManager');
jest.spyOn(TextInputState, 'blurTextInput');
jest.spyOn(TextInputState, 'focusTextInput');
require('../../../../src/private/featureflags/ReactNativeFeatureFlags').override(
{
enableAccessToHostTreeInFabric: () =>
flags.enableAccessToHostTreeInFabric,
},
);
});
describe('blur', () => {
test('blur() invokes TextInputState', async () => {
const result = await mockRenderKeys([['foo']]);
const fooRef = nullthrows(result?.[0]?.[0]);
fooRef.blur();
expect(mockOf(TextInputState.blurTextInput).mock.calls).toEqual([
[fooRef],
]);
});
});
describe('focus', () => {
test('focus() invokes TextInputState', async () => {
const result = await mockRenderKeys([['foo']]);
const fooRef = nullthrows(result?.[0]?.[0]);
fooRef.focus();
expect(mockOf(TextInputState.focusTextInput).mock.calls).toEqual([
[fooRef],
]);
});
});
describe('measure', () => {
itif(!isWindows)('component.measure(...) invokes callback', async () => {
const result = await mockRenderKeys([['foo']]);
const fooRef = nullthrows(result?.[0]?.[0]);
const callback = jest.fn();
fooRef.measure(callback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measure,
).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 0, 0]]);
});
itif(!isWindows)('unmounted.measure(...) does nothing', async () => {
const result = await mockRenderKeys([['foo'], null]);
const fooRef = nullthrows(result?.[0]?.[0]);
const callback = jest.fn();
fooRef.measure(callback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measure,
).not.toHaveBeenCalled();
expect(callback).not.toHaveBeenCalled();
});
});
describe('measureInWindow', () => {
itif(!isWindows)(
'component.measureInWindow(...) invokes callback',
async () => {
const result = await mockRenderKeys([['foo']]);
const fooRef = nullthrows(result?.[0]?.[0]);
const callback = jest.fn();
fooRef.measureInWindow(callback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow,
).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]);
},
);
itif(!isWindows)(
'unmounted.measureInWindow(...) does nothing',
async () => {
const result = await mockRenderKeys([['foo'], null]);
const fooRef = nullthrows(result?.[0]?.[0]);
const callback = jest.fn();
fooRef.measureInWindow(callback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow,
).not.toHaveBeenCalled();
expect(callback).not.toHaveBeenCalled();
},
);
});
describe('measureLayout', () => {
itif(!isWindows)(
'component.measureLayout(component, ...) invokes callback',
async () => {
const result = await mockRenderKeys([['foo', 'bar']]);
const fooRef = nullthrows(result?.[0]?.[0]);
const barRef = nullthrows(result?.[0]?.[1]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
).toHaveBeenCalledTimes(1);
expect(successCallback.mock.calls).toEqual([[1, 1, 100, 100]]);
},
);
itif(!isWindows)(
'unmounted.measureLayout(component, ...) does nothing',
async () => {
const result = await mockRenderKeys([
['foo', 'bar'],
['foo', null],
]);
const fooRef = nullthrows(result?.[0]?.[0]);
const barRef = nullthrows(result?.[0]?.[1]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
},
);
itif(!isWindows)(
'component.measureLayout(unmounted, ...) does nothing',
async () => {
const result = await mockRenderKeys([
['foo', 'bar'],
[null, 'bar'],
]);
const fooRef = nullthrows(result?.[0]?.[0]);
const barRef = nullthrows(result?.[0]?.[1]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
},
);
itif(!isWindows)(
'unmounted.measureLayout(unmounted, ...) does nothing',
async () => {
const result = await mockRenderKeys([['foo', 'bar'], null]);
const fooRef = nullthrows(result?.[0]?.[0]);
const barRef = nullthrows(result?.[0]?.[1]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(
nullthrows(FabricUIManager.getFabricUIManager()).measureLayout,
).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
},
);
});
});
});
@@ -11,11 +11,11 @@
import '../../../Core/InitializeCore.js';
import * as ReactNativeTester from '../../../../src/private/__tests__/ReactNativeTester';
import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement';
import TextInputState from '../../../Components/TextInput/TextInputState';
import View from '../../../Components/View/View';
import ReactFabricHostComponent from '../ReactFabricHostComponent';
import * as Fantom from '@react-native/fantom';
import nullthrows from 'nullthrows';
import * as React from 'react';
@@ -25,8 +25,8 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
it('should provide instances of the right class as refs in host components', () => {
let node;
const root = ReactNativeTester.createRoot();
ReactNativeTester.runTask(() => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View
ref={receivedNode => {
@@ -43,11 +43,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
describe('blur', () => {
test('blur() invokes TextInputState', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
ref={node => {
@@ -64,7 +64,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
// We don't support view commands in Fantom yet, so we have to mock this.
TextInputState.blurTextInput = blurTextInput;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
node.blur();
});
@@ -75,11 +75,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
describe('focus', () => {
test('focus() invokes TextInputState', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
ref={node => {
@@ -96,7 +96,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
// We don't support view commands in Fantom yet, so we have to mock this.
TextInputState.focusTextInput = focusTextInput;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
node.focus();
});
@@ -107,11 +107,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
describe('measure', () => {
it('component.measure(...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -132,11 +132,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
});
it('unmounted.measure(...) does nothing', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -149,7 +149,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
const node = nullthrows(maybeNode);
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(<></>);
});
@@ -162,11 +162,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
describe('measureInWindow', () => {
it('component.measureInWindow(...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -187,11 +187,11 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
});
it('unmounted.measureInWindow(...) does nothing', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -204,7 +204,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
const node = nullthrows(maybeNode);
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(<></>);
});
@@ -217,12 +217,12 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
describe('measureLayout', () => {
it('component.measureLayout(component, ...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -250,12 +250,12 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
});
it('unmounted.measureLayout(component, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -275,7 +275,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100, left: 10, top: 10}} />,
);
@@ -288,12 +288,12 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
});
it('component.measureLayout(unmounted, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -313,7 +313,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100, left: 10, top: 10}} />,
);
@@ -326,12 +326,12 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
});
it('unmounted.measureLayout(unmounted, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
const root = Fantom.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
@@ -351,7 +351,7 @@ export default function setUpTests({isModern}: {isModern: boolean}) {
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
Fantom.runTask(() => {
root.render(<></>);
});
@@ -1,334 +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 type {
InternalInstanceHandle,
LayoutAnimationConfig,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
Node,
} from '../../Renderer/shims/ReactNativeTypes';
import type {RootTag} from '../../Types/RootTagTypes';
import type {
NodeProps,
NodeSet,
Spec as FabricUIManager,
} from '../FabricUIManager';
import {createRootTag} from '../RootTag.js';
export type NodeMock = {
children: NodeSet,
instanceHandle: InternalInstanceHandle,
props: NodeProps,
reactTag: number,
rootTag: RootTag,
viewName: string,
};
export function fromNode(node: Node): NodeMock {
// $FlowExpectedError[incompatible-return]
return node;
}
export function toNode(node: NodeMock): Node {
// $FlowExpectedError[incompatible-return]
return node;
}
// Mock of the Native Hooks
const roots: Map<RootTag, NodeSet> = new Map();
const allocatedTags: Set<number> = new Set();
export function ensureHostNode(node: Node): void {
if (node == null || typeof node !== 'object') {
throw new Error(
`Expected node to be an object. Got ${
node === null ? 'null' : typeof node
} value`,
);
}
if (typeof node.viewName !== 'string') {
throw new Error(
`Expected node to be a host node. Got object with ${
node.viewName === null ? 'null' : typeof node.viewName
} viewName`,
);
}
}
function getAncestorsInChildSet(
node: Node,
childSet: NodeSet,
): ?$ReadOnlyArray<[Node, number]> {
const rootNode = toNode({
reactTag: 0,
rootTag: fromNode(node).rootTag,
viewName: 'RootNode',
// $FlowExpectedError
instanceHandle: null,
props: {},
children: childSet,
});
let position = 0;
for (const child of childSet) {
const ancestors = getAncestors(child, node);
if (ancestors) {
return [[rootNode, position]].concat(ancestors);
}
position++;
}
return null;
}
export function getAncestorsInCurrentTree(
node: Node,
): ?$ReadOnlyArray<[Node, number]> {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
return getAncestorsInChildSet(node, childSet);
}
function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
if (fromNode(root).reactTag === fromNode(node).reactTag) {
return [];
}
let position = 0;
for (const child of fromNode(root).children) {
const ancestors = getAncestors(child, node);
if (ancestors != null) {
return [[root, position]].concat(ancestors);
}
position++;
}
return null;
}
export function getNodeInChildSet(node: Node, childSet: NodeSet): ?Node {
const ancestors = getAncestorsInChildSet(node, childSet);
if (ancestors == null) {
return null;
}
const [parent, position] = ancestors[ancestors.length - 1];
const nodeInCurrentTree = fromNode(parent).children[position];
return nodeInCurrentTree;
}
export function getNodeInCurrentTree(node: Node): ?Node {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
return getNodeInChildSet(node, childSet);
}
interface IFabricUIManagerMock extends FabricUIManager {
getRoot(rootTag: RootTag | number): NodeSet;
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle;
__addCommitHook(commitHook: UIManagerCommitHook): void;
__removeCommitHook(commitHook: UIManagerCommitHook): void;
}
export interface UIManagerCommitHook {
shadowTreeWillCommit: (
rootTag: RootTag,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
) => void;
}
const commitHooks: Set<UIManagerCommitHook> = new Set();
const FabricUIManagerMock: IFabricUIManagerMock = {
createNode: jest.fn(
(
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InternalInstanceHandle,
): Node => {
if (allocatedTags.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
}
allocatedTags.add(reactTag);
return toNode({
reactTag,
rootTag,
viewName,
instanceHandle,
props: props,
children: [],
});
},
),
cloneNode: jest.fn((node: Node): Node => {
return toNode({...fromNode(node)});
}),
cloneNodeWithNewChildren: jest.fn((node: Node): Node => {
return toNode({...fromNode(node), children: []});
}),
cloneNodeWithNewProps: jest.fn((node: Node, newProps: NodeProps): Node => {
return toNode({
...fromNode(node),
props: {
...fromNode(node).props,
...newProps,
},
});
}),
cloneNodeWithNewChildrenAndProps: jest.fn(
(node: Node, newProps: NodeProps): Node => {
return toNode({
...fromNode(node),
children: [],
props: {
...fromNode(node).props,
...newProps,
},
});
},
),
createChildSet: jest.fn((rootTag: RootTag): NodeSet => {
return [];
}),
appendChild: jest.fn((parentNode: Node, child: Node): Node => {
// Although the signature returns a Node, React expects this to be mutating.
fromNode(parentNode).children.push(child);
return parentNode;
}),
appendChildToSet: jest.fn((childSet: NodeSet, child: Node): void => {
childSet.push(child);
}),
completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {
commitHooks.forEach(hook =>
hook.shadowTreeWillCommit(rootTag, roots.get(rootTag), childSet),
);
roots.set(rootTag, childSet);
}),
measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
ensureHostNode(node);
callback(10, 10, 100, 100, 0, 0);
}),
measureInWindow: jest.fn(
(node: Node, callback: MeasureInWindowOnSuccessCallback): void => {
ensureHostNode(node);
callback(10, 10, 100, 100);
},
),
measureLayout: jest.fn(
(
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
): void => {
ensureHostNode(node);
ensureHostNode(relativeNode);
onSuccess(1, 1, 100, 100);
},
),
configureNextLayoutAnimation: jest.fn(
(
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
errorCallback: () => void,
): void => {},
),
sendAccessibilityEvent: jest.fn((node: Node, eventType: string): void => {}),
findShadowNodeByTag_DEPRECATED: jest.fn((reactTag: number): ?Node => {}),
findNodeAtPoint: jest.fn(
(
node: Node,
locationX: number,
locationY: number,
callback: (instanceHandle: ?InternalInstanceHandle) => void,
): void => {},
),
getBoundingClientRect: jest.fn(
(
node: Node,
includeTransform: boolean,
): ?[
/* x:*/ number,
/* y:*/ number,
/* width:*/ number,
/* height:*/ number,
] => {},
),
setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),
dispatchCommand: jest.fn(
(node: Node, commandName: string, args: Array<mixed>): void => {},
),
compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => 0),
getRoot(containerTag: RootTag | number): NodeSet {
const tag = createRootTag(containerTag);
const root = roots.get(tag);
if (!root) {
throw new Error('No root found for containerTag ' + Number(tag));
}
return root;
},
__getInstanceHandleFromNode(node: Node): InternalInstanceHandle {
return fromNode(node).instanceHandle;
},
__addCommitHook(commitHook: UIManagerCommitHook): void {
commitHooks.add(commitHook);
},
__removeCommitHook(commitHook: UIManagerCommitHook): void {
commitHooks.delete(commitHook);
},
};
global.nativeFabricUIManager = FabricUIManagerMock;
export function getFabricUIManager(): ?IFabricUIManagerMock {
return FabricUIManagerMock;
}
@@ -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 View from '../../Components/View/View';
import * as Fantom from '@react-native/fantom';
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 = Fantom.createRoot();
Fantom.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();
Fantom.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`',
);
Fantom.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();
Fantom.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`',
);
Fantom.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 = Fantom.createRoot();
function App(props: {color: 'red' | 'green'}) {
return (
<Suspense fallback={<Fallback />}>
{props.color === 'green' ? <GreenSquare /> : <RedSquare />}
</Suspense>
);
}
Fantom.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();
Fantom.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();
Fantom.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();
});
});
@@ -7,7 +7,7 @@
* @noformat
* @nolint
* @flow strict
* @generated SignedSource<<9cf3e28d6ca0299bc0bb5caa75b19556>>
* @generated SignedSource<<c6ea057ee85cbc116a083e3a306b2b88>>
*/
import type {ElementRef, ElementType, MixedElement} from 'react';
@@ -133,9 +133,10 @@ declare const ensureNativeMethodsAreSynced: NativeMethods;
(ensureNativeMethodsAreSynced: INativeMethods);
export type HostInstance = NativeMethods;
/*::
export type HostComponent<Config: {...}> = component(ref: React$RefSetter<HostInstance>, ...Config);
*/
export type HostComponent<Config: {...}> = component(
ref: React$RefSetter<HostInstance>,
...Config
);
type InspectorDataProps = $ReadOnly<{
[propName: string]: string,
@@ -206,9 +207,7 @@ export type ReactNativeType = {
componentOrHandle: ?(ElementRef<TElementType> | number),
): ?number,
isChildPublicInstance(
// eslint-disable-next-line no-undef
parent: PublicInstance | HostComponent<empty>,
// eslint-disable-next-line no-undef
child: PublicInstance | HostComponent<empty>,
): boolean,
dispatchCommand(
@@ -148,6 +148,12 @@ function _validateTransforms(transform: Array<Object>): void {
);
const key = keys[0];
const value = transformation[key];
if (key === 'matrix' && transform.length > 1) {
console.error(
'When using a matrix transform, you must specify exactly one transform object. Passed transform: ' +
stringifySafe(transform),
);
}
_validateTransform(key, value, transformation);
});
}
+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;
}
/**
@@ -66,6 +66,15 @@ typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) {
NSLayoutManager *layoutManager = self.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
// A workaround for truncatedGlyphRangeInLineFragmentForGlyphAtIndex returning NSNotFound when text has only
// one character and it gets truncated
if ([self length] == 1) {
CGSize characterSize = [[self string] sizeWithAttributes:[self attributesAtIndex:0 effectiveRange:nil]];
if (characterSize.width > size.width) {
return RCTTextSizeComparisonLarger;
}
}
[layoutManager ensureLayoutForTextContainer:textContainer];
// Does it fit the text container?
@@ -476,7 +476,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length,
0);
if (text.length > _maxLength.integerValue) {
if (text.length > allowedLength) {
// If we typed/pasted more than one character, limit the text inputted.
if (text.length > 1) {
if (allowedLength > 0) {
+2 -2
View File
@@ -17,7 +17,7 @@ import type {
AccessibilityState,
Role,
} from '../Components/View/ViewAccessibility';
import type {TextStyleProp} from '../StyleSheet/StyleSheet';
import type {ColorValue, TextStyleProp} from '../StyleSheet/StyleSheet';
import type {
LayoutEvent,
PointerEvent,
@@ -212,7 +212,7 @@ export type TextProps = $ReadOnly<{
*
* See https://reactnative.dev/docs/text#selectioncolor
*/
selectionColor?: ?string,
selectionColor?: ?ColorValue,
dataDetectorType?: ?('phoneNumber' | 'link' | 'email' | 'none' | 'all'),
@@ -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,
@@ -447,6 +449,7 @@ declare export default class Animation {
__findAnimatedPropsNodes(node: AnimatedNode): Array<AnimatedProps>;
__startAnimationIfNative(animatedValue: AnimatedValue): boolean;
__notifyAnimationEnd(result: EndResult): void;
__getDebugID(): ?string;
}
"
`;
@@ -770,7 +773,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
"declare export default class AnimatedAddition extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number): void;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): number;
interpolate<OutputT: number | string>(
@@ -785,6 +792,7 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedColor.js 1`] = `
"export type AnimatedColorConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
type ColorListenerCallback = (value: ColorValue) => mixed;
@@ -835,7 +843,12 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
_max: number;
_value: number;
_lastValue: number;
constructor(a: AnimatedNode, min: number, max: number): void;
constructor(
a: AnimatedNode,
min: number,
max: number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>
@@ -853,7 +866,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
_a: AnimatedNode;
_b: AnimatedNode;
_warnedAboutDivideByZero: boolean;
constructor(a: AnimatedNode | number, b: AnimatedNode | number): void;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): number;
interpolate<OutputT: number | string>(
@@ -869,6 +886,7 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedInterpolation.js 1`] = `
"type ExtrapolateType = \\"extend\\" | \\"identity\\" | \\"clamp\\";
export type InterpolationConfigType<OutputT: number | string> = $ReadOnly<{
...AnimatedNodeConfig,
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
easing?: (input: number) => number,
@@ -903,7 +921,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
"declare export default class AnimatedModulo extends AnimatedWithChildren {
_a: AnimatedNode;
_modulus: number;
constructor(a: AnimatedNode, modulus: number): void;
constructor(
a: AnimatedNode,
modulus: number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): number;
interpolate<OutputT: number | string>(
@@ -922,7 +944,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
{
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number): void;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): number;
interpolate<OutputT: number | string>(
@@ -936,8 +962,17 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
`;
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedNode.js 1`] = `
"declare export default class AnimatedNode {
"export type AnimatedNodeConfig = $ReadOnly<{
debugID?: string,
}>;
declare export default class AnimatedNode {
_platformConfig: ?PlatformConfig;
constructor(
config?: ?$ReadOnly<{
...AnimatedNodeConfig,
...
}>
): void;
__attach(): void;
__detach(): void;
__getValue(): any;
@@ -959,6 +994,8 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
__getPlatformConfig(): ?PlatformConfig;
__setPlatformConfig(platformConfig: ?PlatformConfig): void;
toJSON(): mixed;
__debugID: ?string;
__getDebugID(): ?string;
}
"
`;
@@ -970,7 +1007,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
declare export default class AnimatedObject extends AnimatedWithChildren {
_value: mixed;
static from(value: mixed): ?AnimatedObject;
constructor(nodes: $ReadOnlyArray<AnimatedNode>, value: mixed): void;
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
value: mixed,
config?: ?AnimatedNodeConfig
): void;
__getValue(): any;
__getValueWithStaticObject(staticObject: mixed): any;
__getAnimatedValue(): any;
@@ -991,7 +1032,8 @@ declare export default class AnimatedProps extends AnimatedNode {
constructor(
inputProps: { [string]: mixed },
callback: () => void,
allowlist?: ?AnimatedPropsAllowlist
allowlist?: ?AnimatedPropsAllowlist,
config?: ?AnimatedNodeConfig
): void;
__getValue(): Object;
__getValueWithStaticProps(staticProps: Object): Object;
@@ -1020,7 +1062,8 @@ declare export default class AnimatedStyle extends AnimatedWithChildren {
nodeKeys: $ReadOnlyArray<string>,
nodes: $ReadOnlyArray<AnimatedNode>,
style: { [string]: mixed },
inputStyle: any
inputStyle: any,
config?: ?AnimatedNodeConfig
): void;
__getValue(): Object | Array<Object>;
__getValueWithStaticStyle(staticStyle: Object): Object | Array<Object>;
@@ -1037,7 +1080,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
"declare export default class AnimatedSubtraction extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number): void;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): number;
interpolate<OutputT: number | string>(
@@ -1063,7 +1110,8 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
parent: AnimatedNode,
animationClass: any,
animationConfig: Object,
callback?: ?EndCallback
callback?: ?EndCallback,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): Object;
@@ -1089,7 +1137,8 @@ declare export default class AnimatedTransform extends AnimatedWithChildren {
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform;
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
transforms: $ReadOnlyArray<Transform<>>
transforms: $ReadOnlyArray<Transform<>>,
config?: ?AnimatedNodeConfig
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): $ReadOnlyArray<Transform<any>>;
@@ -1106,6 +1155,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<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
declare export function flushValue(rootNode: AnimatedNode): void;
@@ -1139,6 +1189,7 @@ declare export default class AnimatedValue extends AnimatedWithChildren {
exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedValueXY.js 1`] = `
"export type AnimatedValueXYConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
type ValueXYListenerCallback = (value: { x: number, y: number, ... }) => mixed;
@@ -6669,15 +6720,7 @@ declare export default typeof NativeNetworkingIOS;
`;
exports[`public API should not change unintentionally Libraries/Network/RCTNetworking.js.flow 1`] = `
"type RCTNetworkingEventDefinitions = $ReadOnly<{
didSendNetworkData: [[number, number, number]],
didReceiveNetworkResponse: [[number, number, ?{ [string]: string }, ?string]],
didReceiveNetworkData: [[number, string]],
didReceiveNetworkIncrementalData: [[number, string, number, number]],
didReceiveNetworkDataProgress: [[number, number, number]],
didCompleteNetworkResponse: [[number, string, boolean]],
}>;
declare const RCTNetworking: interface {
"declare const RCTNetworking: interface {
addListener<K: $Keys<RCTNetworkingEventDefinitions>>(
eventType: K,
listener: (...$ElementType<RCTNetworkingEventDefinitions, K>) => mixed,
@@ -6702,6 +6745,18 @@ declare export default typeof RCTNetworking;
"
`;
exports[`public API should not change unintentionally Libraries/Network/RCTNetworkingEventDefinitions.flow.js 1`] = `
"export type RCTNetworkingEventDefinitions = $ReadOnly<{
didSendNetworkData: [[number, number, number]],
didReceiveNetworkResponse: [[number, number, ?{ [string]: string }, ?string]],
didReceiveNetworkData: [[number, string]],
didReceiveNetworkIncrementalData: [[number, string, number, number]],
didReceiveNetworkDataProgress: [[number, number, number]],
didCompleteNetworkResponse: [[number, string, boolean]],
}>;
"
`;
exports[`public API should not change unintentionally Libraries/Network/XHRInterceptor.js 1`] = `
"type XHRInterceptorOpenCallback = (
method: string,
@@ -8523,7 +8578,7 @@ export type TextProps = $ReadOnly<{
style?: ?TextStyleProp,
testID?: ?string,
disabled?: ?boolean,
selectionColor?: ?string,
selectionColor?: ?ColorValue,
dataDetectorType?: ?(\\"phoneNumber\\" | \\"link\\" | \\"email\\" | \\"none\\" | \\"all\\"),
textBreakStrategy?: ?(\\"balanced\\" | \\"highQuality\\" | \\"simple\\"),
adjustsFontSizeToFit?: ?boolean,
@@ -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];
}

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