Compare commits

...
Author SHA1 Message Date
Blake Friedman f99108e161 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]


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

Reviewed By: robhogan

Differential Revision: D66657082

Pulled By: blakef
2024-12-02 09:52:34 -08:00
Alex Hunt 1a9780f0e3 Remove FuseboxClient CDP domain (#48004)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48004

Follows https://github.com/facebook/react-native/pull/47962 and depends on https://github.com/facebookexperimental/rn-chrome-devtools-frontend/pull/139.

Updates the modern debugger server to no longer respond to `FuseboxClient` messages — namely `FuseboxClient.setClientMetadata`. This method is replaced by `ReactNativeApplication.enable` for identifying the React Native DevTools frontend.

Changelog:
[General][Breaking] - The `FuseboxClient.setClientMetadata` CDP method is removed. Instead, use `ReactNativeApplication.enable`.

Reviewed By: rubennorte

Differential Revision: D66575324

fbshipit-source-id: f2b4cbacd857931832d89305510f5aaf51df483a
2024-12-02 08:27:52 -08:00
Peter Abbondanzo 72bb2f4089 Bump Fresco to 3.5.0
Summary:
Updates Fresco from 3.4.0 to 3.5.0. Picks up a few new features, including experimental support for XML-based drawable resource types

Changelog:
[Android][Changed] - Update Fresco to 3.5.0

Reviewed By: cortinico

Differential Revision: D66553841

fbshipit-source-id: d0e630c73ba73ea9bbf96f7d630471c5383145f0
2024-12-02 07:33:37 -08:00
Pieter De Baets 70a957452c Restore deprecated TurboReactPackage (#48039)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48039

This class was removed in D66127067 but was marked as DeprecatedInNewArchitecture and not Deprecated, which limited the signal we gave to developers to move away from this.

Restore for now to e

Changelog: [Android][Fixed] Reverted removal of TurboReactPackage

Reviewed By: rshest

Differential Revision: D66648209

fbshipit-source-id: 165f9390b4874e69353612b929d87b0c495588af
2024-12-02 07:30:52 -08:00
Alex Hunt a4310ede9c Update debugger-frontend from b61aae3...6b80704 (#48042)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48042

Changelog: [Internal] - Update `react-native/debugger-frontend` from b61aae3...6b80704

Resyncs `react-native/debugger-frontend` from GitHub - see `rn-chrome-devtools-frontend` [changelog](https://github.com/facebookexperimental/rn-chrome-devtools-frontend/compare/b61aae3ccc6e2684dfbf1e2a06b0f985b459f11f...6b80704fd50ea0bf10f5f5da5a4343de29aff8b2).

Reviewed By: blakef

Differential Revision: D66651149

fbshipit-source-id: 6848eebb4b7c04c7c04ae1f784fc39785945bf7b
2024-12-02 06:25:54 -08:00
Rubén Norte 0580e88aa5 Allow tests to specify opt/dev mode (#48022)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48022

Changelog: [internal]

This adds support for Fantom options in tests to configure different aspects of the test execution.

For now, it only supports specifying the mode (dev or opt) so we can try things without having to change the runner (watch mode still works if you change mode :D).

Fantom options are specified as pragmas in the docblock of the test. E.g.:

```
/**
 * flow strict-local
 * format
 * fantom_mode opt
 */
```

We expect this is mostly going to be used for one-time tests and that regular tests won't specify the mode (they'll just run in dev mode).

Maybe we can evolve this in the future to specify that you want a test to be executed in both modes, to ensure the behavior is consistent in dev/prod.

Reviewed By: rshest

Differential Revision: D66597626

fbshipit-source-id: b12325fc2235740cc2a3e0283d6a556091c1794c
2024-12-02 06:06:26 -08:00
Rubén Norte 762389f775 Extract logic to debug command errors to shared function (#48019)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48019

Changelog: [internal]

Just a minor refactor to reduce code duplication.

Reviewed By: rshest

Differential Revision: D66596730

fbshipit-source-id: 2afa501cd5664402adf4aceba059a4d987c3ed62
2024-12-02 06:06:26 -08:00
Rubén Norte 4bced9099a Implement warm up step to remove costly builds from individual test running time (#48015)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48015

Changelog: [internal]

Right now, when we run individual Fantom tests, we compile Hermes and the RN Tester CLI as part of the test, which causes the first test to run to be very slow and the remaining tests in the same run to be very fast.

This is misleading because it makes it look like the test itself is slow, when it's actually paying a price for everyone.

Fortunately, Jest has an option to do a global setup before any tests in the project run (and it doesn't run if none of the tests in the project run, in multi-project setups), so we can use it to do the necessary warmup so it doesn't end up being attributed to individual tests.

Reviewed By: javache

Differential Revision: D66595406

fbshipit-source-id: 496aa2b248da661f7504c8445fed1edad0301803
2024-12-02 06:06:26 -08:00
Rubén Norte 21cc745d51 Extract some logic from runner to utils module (#48014)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48014

Changelog: [internal]

Just a small refactor in preparation for a following change that will add more usages for these utilities. It also cleans up the runner file which is good too.

Reviewed By: javache

Differential Revision: D66595405

fbshipit-source-id: e734d76006ce937fadd1cb673035db85a3e838dd
2024-12-02 06:06:26 -08:00
Vojtech Novak 9147b0753a fix IOException in BuildCodegenCLITask (#48008)
Summary:
building RN tester with 0.77 rc-0 doesn't work now because of `java.io.IOException:  No such file or directory` on line 48.

`buildDirectory` is a Gradle property representing a file

https://github.com/facebook/react-native/pull/47552 removes this file altogether so feel free to close if that one is the "right one"

## Changelog:

[ANDROID] [FIXED] - fix IOException in `BuildCodegenCLITask`

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

Test Plan: After this change, building RN tester works.

Reviewed By: cortinico

Differential Revision: D66650038

Pulled By: robhogan

fbshipit-source-id: 11cd83493fa118c6b79d11c9113228dd3971a803
2024-12-02 05:09:54 -08:00
Rubén Norte 2480de6ffb Add Fantom test for LongTask API (#48041)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48041

Changelog: [internal]

This adds a Fantom test for the LongTask API, testing it using public APIs :D

Reviewed By: javache

Differential Revision: D66601861

fbshipit-source-id: f3531e8b58ffa044dcb5cec2f462ae6a31c27790
2024-12-02 04:47:17 -08:00
Rubén Norte b6f2b148f4 Add suppor for toBeLessThanOrEqual and toBeGreaterThanOrEqual (#48040)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48040

Changelog: [internal]

We need more expectations for a new test we're writing. This just adds:
* `expect(received).toBeLessThanOrEqual(expected)`
* `expect(received).toBeGreaterThanOrEqual(expected)`

Reviewed By: sammy-SC

Differential Revision: D66601921

fbshipit-source-id: 0a73f7757117ed790b95796b259244c8259136b7
2024-12-02 04:47:17 -08:00
Rubén Norte 60a4d22307 Migrate ReactFabricPublicInstance tests to Fantom (#48025)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48025

Changelog: [internal]

This migrates the existing tests we have for the current public API for host component refs to Fantom.

After this, the only remaining test to migrate before we can clean up the legacy mocks for Fabric, etc. is the one for IntersectionObserer.

Reviewed By: sammy-SC

Differential Revision: D66599070

fbshipit-source-id: 67da1cd3b360ac79aed6fe6ad2a8bd5273754174
2024-12-02 04:47:17 -08:00
Rubén Norte 09f6d165ec Export ReactNativeTester (#48024)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48024

Changelog: [internal]

Reviewed By: rshest

Differential Revision: D66599071

fbshipit-source-id: 52585e030642e5b6bd8921d2c9f9d14ee5d6ce71
2024-12-02 04:47:17 -08:00
Nicola Corti 88c9a42fca Fix eslint warnings in react-native (#48016)
Summary:
There are currently 2 warnings firing for every PR (e.g. look here https://github.com/facebook/react-native/pull/48013/files).

Those are annoying so I'm fixing them here.

## Changelog:

[INTERNAL] - Fix eslint warnings in react-native

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

Test Plan: CI

Reviewed By: NickGerleman

Differential Revision: D66595842

Pulled By: cortinico

fbshipit-source-id: 0fd39629a97dbbe5d75a78c8eaa50241faf6bf1e
2024-12-02 04:11:22 -08:00
zhongwuzw 28ced2e558 Fabric: Fixes Modal onRequestClose not called (#48037)
Summary:
Fixes https://github.com/facebook/react-native/issues/48030 .

## Changelog:

[IOS] [FIXED] - Fabric: Fixes Modal onRequestClose not called

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

Test Plan: Repro please see  https://github.com/facebook/react-native/issues/48030.

Reviewed By: cortinico

Differential Revision: D66647232

Pulled By: cipolleschi

fbshipit-source-id: 773517dfe45f6f2e6348cda225e972fbac05edc2
2024-12-02 04:06:40 -08:00
CHOIMINSEOK b8095f4692 Avoid NPE when touch event is triggered before SurfaceManager is initiated (#48007)
Summary:
A NPE can occur when a user touches the screen before the `SurfaceMountingManager` is initialized. Below is an example of the error log from our production service. This issue can also be reproduced using RNTester. To prevent invalid touch events during init time of rn app from causing an NPE, add a null check for SurfaceMountingManager before calling mark/sweepActiveTouchForTag.

```
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void com.facebook.react.fabric.mounting.SurfaceMountingManager.markActiveTouchForTag(int)' on a null object reference
       at com.facebook.react.fabric.FabricUIManager.markActiveTouchForTag(FabricUIManager.java)
       at com.facebook.react.uimanager.JSTouchDispatcher.markActiveTouchForTag(JSTouchDispatcher.java)
       at com.facebook.react.uimanager.JSTouchDispatcher.handleTouchEvent(JSTouchDispatcher.java)
       at com.facebook.react.runtime.ReactSurfaceView.dispatchJSTouchEvent(ReactSurfaceView.java)
       at com.facebook.react.ReactRootView.onInterceptTouchEvent(ReactRootView.java)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2870)
       at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3352)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2963)
       at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3352)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2963)
       at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3352)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2963)
       at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3352)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2963)
       at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3352)
       at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2963)
       at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:794)
       at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1967)
       at android.app.Activity.dispatchTouchEvent(Activity.java:4571)
       at com.rainist.banksalad2.feature.common.BaseActivity.dispatchTouchEvent(BaseActivity.java)
       at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:70)
       at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:752)
       at android.view.View.dispatchPointerEvent(View.java:16498)
       at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:8676)
       at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:8423)
       at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7752)
       at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7809)
       at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7775)
       at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7978)
       at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7783)
       at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:8035)
       at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7756)
       at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7809)
       at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7775)
       at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7783)
       at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7756)
       at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:11343)
       at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:11212)
       at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:11168)
       at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:11477)
       at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:337)
       at android.os.MessageQueue.nativePollOnce(MessageQueue.java)
       at android.os.MessageQueue.next(MessageQueue.java:335)
       at android.os.Looper.loopOnce(Looper.java:187)
       at android.os.Looper.loop(Looper.java:319)
       at android.app.ActivityThread.main(ActivityThread.java:9063)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:588)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)
```
https://github.com/user-attachments/assets/e9c6ff84-c94d-4392-9042-8e635197202e

## Changelog:

[Android] [Fixed] - Avoid NPE when touch event is triggered before SurfaceManager is initiated

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

Test Plan:
I checked the crashed being fixed on RNTester.

https://github.com/user-attachments/assets/71f7e359-707a-494c-ae34-fef8d432e612

Reviewed By: cortinico

Differential Revision: D66594576

Pulled By: javache

fbshipit-source-id: b1559d94866bdb021e0374f1953684849603033c
2024-12-02 03:54:34 -08:00
Wojciech Lewicki 44b04b6d42 fix: add mising subpsec to React-Fabric podspec (#48023)
Summary:
Running app with static linking fails to compile. Probably during https://github.com/facebook/react-native/pull/43581 adding that code was overlooked since the analogous thing seems to be added: https://github.com/facebook/react-native/pull/43581/files#diff-6680f6849631e3dcc5897ee3961a9d6d2bc57aff3eccb79a9d9c634183276202R566.

cc rubennorte since you made the linked 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] [FIXED] - Add missing subpsec to React-Fabric podspec.

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

Test Plan: Run https://github.com/WoLewicki/reproducer-react-native/tree/%40wolewicki/static-linking-with-live-markdown and see that it won't compile without this change.

Reviewed By: cipolleschi

Differential Revision: D66600486

Pulled By: cortinico

fbshipit-source-id: 9de64541e49d27cdf00f37942adab6620476f15a
2024-12-02 03:23:45 -08:00
Wojciech Lewicki fa03840e68 fix: typo in inherited (#48013)
Summary:
fix typo in `inherithed`

## Changelog:

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

Pick one each for the category and type tags:

[IOS] [CHANGED] - fix typo in utils.rb

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

[IOS] [CHANGED] - fix typo in utils.rb

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

Test Plan: I did not spot any changes when switching it but in some specific build settings it could destroy some behaviors probably.

Reviewed By: cipolleschi

Differential Revision: D66595786

Pulled By: cortinico

fbshipit-source-id: d1607fd1127352533fb2977bdfcafec1edd1aef7
2024-12-02 03:23:01 -08:00
Nicola Corti 490db92562 Gradle to 8.11.1 (#48026)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48026

This should mitigate this particular issue we're seeing on Windows:
- https://github.com/facebook/react-native/issues/46210

Changelog:
[Android] [Changed] - Gradle to 8.11.1

Reviewed By: javache

Differential Revision: D66600321

fbshipit-source-id: d58437485222e189d90bcf4d6b41ca956449ed22
2024-12-02 03:22:32 -08:00
Rob Hogan e996b3f346 Fix Animated on JSC: Object.hasOwn -> obj.hasOwnProperty (#48035)
Summary:
https://github.com/facebook/react-native/pull/46385 introduced use of `Object.hasOwn` as an incidental detail of some `Animated` performance improvements.

Unfortunately, `Object.hasOwn` is not present in the version of JSC shipped with Android, nor the built in iOS JSC until iOS 15.4, which is greater than React Native's minimum version (13.4).

Instead:
 - Use `obj.hasOwnProperty(prop)` for known objects that have the `Object` prototype.
 - Otherwise, use `Object.hasOwn` where it is defined.
 - Lastly, fall back to `Object.prototype.hasOwnProperty.call(obj, prop)`, which is compatible with passed `null`-prototype objects.

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

Intend to pick for RN 0.77.

## Changelog:

[GENERAL][FIXED] Replace Object.hasOwn usages to fix Animated on JSC

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

Test Plan:
- Run `rn-tester` on Android with Hermes disabled.
 - Verify the FlatList->Basic example redboxes before this change, and works after it.

Reviewed By: yungsters

Differential Revision: D66638379

Pulled By: robhogan

fbshipit-source-id: 51ac525851b41adea3bf3cc41349225138e1f2fe
2024-12-02 03:16:28 -08:00
Hugo FOYART f791fb9e66 fix: FormData filename in content-disposition (#46543)
Summary:
This Pull Request fixes a regression introduced in https://github.com/facebook/react-native/commit/7c7e9e6571c1f702213e9ffbb40921cd5a1a786b, which adds a `filename*` attribute to the `content-disposition` of a FormData part. However, as the [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives) states, there is no `filename*` attribute for the `content-disposition` header in case of a form data.

The `filename*` attribute would break the parsing of form data in the request, such as in frameworks like `Next.js` which uses the web implementation of [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request).

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

## 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] [Fixed] - Remove non compliant `filename*` attribute in a FormData `content-disposition` header

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

Test Plan:
- Clone the `react-native` repo
- Create a simple JS file that will act as a node server and execute it

```javascript
const http = require('http');

const server = http.createServer(async function (r, res) {
    const req = new Request(new URL(r.url, 'http://localhost:3000'), {
      headers: r.headers,
      method: r.method,
      body: r,
      duplex: 'half',
    });

    const fileData = await req.formData();

    console.log(fileData);
    res.writeHead(200);
    res.end();
});
server.listen(3000);
```

- Go to `packages/rn-tester`
- Add a `useEffect` in `js/RNTesterAppShared.js`

```javascript
React.useEffect(() => {
    const formData = new FormData();
    formData.append('file', {
      uri: 'https://www.gravatar.com/avatar',
      name: '测试photo/1.jpg',
      type: 'image/jpeg',
    });

    fetch('http://localhost:3000', {
      method: 'POST',
      body: formData,
    }).then(res => console.log(res.ok));
  });
```

- Run the app on iOS or Android
- The node server should output the file added to the FormData with an encoded name

Reviewed By: robhogan

Differential Revision: D66643317

Pulled By: yungsters

fbshipit-source-id: 0d531528005025bff303505363671e854c0a2b63
2024-12-02 03:14:28 -08:00
Kacper Rozniata b886bc4db9 feat(android): migrate ReactSwitchManager to Kotlin (#48003)
Summary:
This PR migrates `ReactSwitchManager` to Kotlin

Also it moves it's shadow node to a separate file (`ReactSwitchShadowNode.kt`)

## Changelog:

[ANDROID] [CHANGED] - Migrate `ReactSwitchManager` to Kotlin

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

Test Plan: Make sure that `Switch` example in `RNTester` works correctly

Reviewed By: cortinico

Differential Revision: D66594606

Pulled By: javache

fbshipit-source-id: 774641c4cf57d6d5f770df1fed4fcafef2af7ceb
2024-12-02 02:41:31 -08:00
zhongwuzw 91e217ff54 Add completion block when call js module function (#47998)
Summary:
Fabric bridgeless don't call completion block when call js module method, so let's support it :).

## Changelog:

[IOS] [FIXED] - [Fabric] Add completion block when call js module function

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

Test Plan: Module call js module method when passed `onComplete` block can be called successfully in bridgeless mode.

Reviewed By: fabriziocucci

Differential Revision: D66594514

Pulled By: javache

fbshipit-source-id: 74644b1f359a24cfa93389451e172fbc6a8ee1a1
2024-11-29 04:08:52 -08:00
Andrew Datsenko 5ff59b448b Add defaults to ParagraphAttributes::getDebugProps (#47986)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47986

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66542033

fbshipit-source-id: d1f3d1776138d076636765262e27ae8e5e7342c5
2024-11-28 12:07:56 -08:00
Andrew Datsenko de30f408e5 Add defaults to TextAttributes::getDebugProps (#47985)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47985

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66542032

fbshipit-source-id: ccecd660f00d3d32909a57534897ba110d53d7b3
2024-11-28 12:07:56 -08:00
Andrew Datsenko 212a743fdf Add float comparison to debugStringConvertilbeUtils (#47984)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47984

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D66541322

fbshipit-source-id: 1adb52de5c9b0321e328532c4394d8067ffce3d8
2024-11-28 12:07:56 -08:00
Andrew Datsenko b27bd00a38 add jest-diff to dependencies (#47990)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47990

Changelog:
[General][Added] - add `jest-diff v29.7.0` to devDependencies

Reviewed By: NickGerleman

Differential Revision: D66541001

fbshipit-source-id: 01c59a936b66f85ce034b59c7928df3c3f8c2a01
2024-11-28 10:04:46 -08:00
lihaitao 2aa79979d3 fix:setColorScheme exception when activity recreate (#47955)
Summary:
solve the exception  on android when `Appearance.setColorScheme` and activity recreate()
Fixes https://github.com/facebook/react-native/issues/47954

## Changelog:

[Android][Fixed] setColorScheme should be called on the UI thread

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

Reviewed By: cortinico

Differential Revision: D66573373

Pulled By: javache

fbshipit-source-id: 97808e163e1c53bb94f4be7269d9cb9e212f2e95
2024-11-28 09:37:07 -08:00
Alex Hunt 2fcf7b1f49 Allow fuseboxClientType_ detection from ReactNativeApplication.enable method (#47962)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47962

Updates `HostAgent` to respond equivalently if either `FuseboxClient.setClientMetadata` (outgoing) or `ReactNativeApplication.enable` (incoming) are sent by the CDP frontend.

This is a partial migration, to be followed by removing the `FuseboxClient.setClientMetadata` method later.

Changelog: [Internal]

Reviewed By: robhogan

Differential Revision: D66501027

fbshipit-source-id: 1ff669c24667f51d240311e75f95747efe577e2d
2024-11-28 09:26:03 -08:00
Rubén Norte 7ccc5934d0 Implement symbolication of error stack traces (#48006)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48006

Changelog: [internal]

This implements symbolication of error stack traces in Fantom tests. We just needed to ask Metro to generate source maps and use the `source-map` package to process the stack traces that we get back from the runtime.

Reviewed By: sammy-SC

Differential Revision: D66577818

fbshipit-source-id: 672c66c246ad8646646d5ed31cabca39eb4f7aca
2024-11-28 07:59:26 -08:00
Pieter De Baets bc9e4db9e9 Demonstrate bug in differentiator where reparented nodes reference non-existent nodes (#48002)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48002

Demonstrates the issue identified in https://github.com/facebook/react-native/issues/47960 and a crash we've been seeing internally around `getViewState` referencing a view that does not exist.

When reparenting unflattened nodes, Differentiator may emit an `update` with a `parentShadowView` that does not exist on the native side yet, thereby crashing Android.

Landing the test-case first (with some test cleanup), so the diff for the actual fix is clearer.

Changelog: [Internal]

Reviewed By: lenaic

Differential Revision: D66557919

fbshipit-source-id: 5428c32e5f0200a8e98568cabeedb0c61aafbe23
2024-11-28 06:06:59 -08:00
53 changed files with 1672 additions and 661 deletions
+40
View File
@@ -0,0 +1,40 @@
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict
* @format
* @oncall react_native
*/
declare module 'jest-diff' {
import type {CompareKeys} from 'pretty-format';
declare export type DiffOptionsColor = (arg: string) => string; // subset of Chalk type
declare export type DiffOptions = {
aAnnotation?: string,
aColor?: DiffOptionsColor,
aIndicator?: string,
bAnnotation?: string,
bColor?: DiffOptionsColor,
bIndicator?: string,
changeColor?: DiffOptionsColor,
changeLineTrailingSpaceColor?: DiffOptionsColor,
commonColor?: DiffOptionsColor,
commonIndicator?: string,
commonLineTrailingSpaceColor?: DiffOptionsColor,
contextLines?: number,
emptyFirstOrLastLinePlaceholder?: string,
expand?: boolean,
includeChangeCounts?: boolean,
omitAnnotationLines?: boolean,
patchColor?: DiffOptionsColor,
compareKeys?: CompareKeys,
};
declare export function diff(
a: mixed,
b: mixed,
options?: DiffOptions,
): string | null;
}
+4 -1
View File
@@ -19,7 +19,6 @@ declare type Colors = {
tag: {close: string, open: string},
value: {close: string, open: string},
};
declare type CompareKeys = ((a: string, b: string) => number) | null | void;
declare type PrettyFormatPlugin =
| {
@@ -38,6 +37,10 @@ declare type PrettyFormatPlugin =
};
declare module 'pretty-format' {
declare export type CompareKeys =
| ((a: string, b: string) => number)
| null
| void;
declare export function format(
value: mixed,
options?: ?{
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1
View File
@@ -25,4 +25,5 @@ module.exports = {
transformIgnorePatterns: ['.*'],
testRunner: './jest/integration/runner/index.js',
watchPathIgnorePatterns: ['<rootDir>/jest/integration/build/'],
globalSetup: './jest/integration/runner/warmup/index.js',
};
+40 -101
View File
@@ -12,25 +12,29 @@
import type {TestSuiteResult} from '../runtime/setup';
import entrypointTemplate from './entrypoint-template';
import {spawnSync} from 'child_process';
import crypto from 'crypto';
import {
getBuckModeForPlatform,
getDebugInfoFromCommandResult,
getFantomTestConfig,
getShortHash,
runBuck2,
symbolicateStackTrace,
} from './utils';
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {formatResultsErrors} from 'jest-message-util';
import Metro from 'metro';
import nullthrows from 'nullthrows';
import os from 'os';
import path from 'path';
const BUILD_OUTPUT_PATH = path.resolve(__dirname, '..', 'build');
const ENABLE_OPTIMIZED_MODE: false = false;
const PRINT_FANTOM_OUTPUT: false = false;
function parseRNTesterCommandResult(
commandArgs: $ReadOnlyArray<string>,
result: ReturnType<typeof spawnSync>,
): {logs: string, testResult: TestSuiteResult} {
function parseRNTesterCommandResult(result: ReturnType<typeof runBuck2>): {
logs: string,
testResult: TestSuiteResult,
} {
const stdout = result.stdout.toString();
const outputArray = stdout
@@ -46,51 +50,26 @@ function parseRNTesterCommandResult(
testResult = JSON.parse(nullthrows(testResultJSON));
} catch (error) {
throw new Error(
[
'Failed to parse test results from RN tester binary result. Full output:',
'buck2 ' + commandArgs.join(' '),
'stdout:',
stdout,
'stderr:',
result.stderr.toString(),
].join('\n'),
'Failed to parse test results from RN tester binary result.\n' +
getDebugInfoFromCommandResult(result),
);
}
return {logs: outputArray.join('\n'), testResult};
}
function getBuckModeForPlatform() {
const mode = ENABLE_OPTIMIZED_MODE ? 'opt' : 'dev';
switch (os.platform()) {
case 'linux':
return `@//arvr/mode/linux/${mode}`;
case 'darwin':
return os.arch() === 'arm64'
? `@//arvr/mode/mac-arm/${mode}`
: `@//arvr/mode/mac/${mode}`;
case 'win32':
return `@//arvr/mode/win/${mode}`;
default:
throw new Error(`Unsupported platform: ${os.platform()}`);
}
}
function getShortHash(contents: string): string {
return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8);
}
function generateBytecodeBundle({
sourcePath,
bytecodePath,
isOptimizedMode,
}: {
sourcePath: string,
bytecodePath: string,
isOptimizedMode: boolean,
}): void {
const hermesCompilerCommandArgs = [
const hermesCompilerCommandResult = runBuck2([
'run',
getBuckModeForPlatform(),
getBuckModeForPlatform(isOptimizedMode),
'//xplat/hermes/tools/hermesc:hermesc',
'--',
'-emit-binary',
@@ -100,33 +79,10 @@ function generateBytecodeBundle({
'-out',
bytecodePath,
sourcePath,
];
const hermesCompilerCommandResult = spawnSync(
'buck2',
hermesCompilerCommandArgs,
{
encoding: 'utf8',
env: {
...process.env,
PATH: `/usr/local/bin:${process.env.PATH ?? ''}`,
},
},
);
]);
if (hermesCompilerCommandResult.status !== 0) {
throw new Error(
[
'Failed to run Hermes compiler. Full output:',
'buck2 ' + hermesCompilerCommandArgs.join(' '),
'stdout:',
hermesCompilerCommandResult.stdout,
'stderr:',
hermesCompilerCommandResult.stderr,
'error:',
hermesCompilerCommandResult.error,
].join('\n'),
);
throw new Error(getDebugInfoFromCommandResult(hermesCompilerCommandResult));
}
}
@@ -139,7 +95,9 @@ module.exports = async function runTest(
): mixed {
const startTime = Date.now();
const isOptimizedMode = ENABLE_OPTIMIZED_MODE;
const testConfig = getFantomTestConfig(testPath);
const isOptimizedMode = testConfig.mode === 'opt';
const metroConfig = await Metro.loadConfig({
config: path.resolve(__dirname, '..', 'config', 'metro.config.js'),
@@ -163,76 +121,54 @@ module.exports = async function runTest(
fs.mkdirSync(path.dirname(entrypointPath), {recursive: true});
fs.writeFileSync(entrypointPath, entrypointContents, 'utf8');
const sourceMapPath = path.join(
path.dirname(testJSBundlePath),
path.basename(testJSBundlePath, '.js') + '.map',
);
await Metro.runBuild(metroConfig, {
entry: entrypointPath,
out: testJSBundlePath,
platform: 'android',
minify: isOptimizedMode,
dev: !isOptimizedMode,
sourceMap: true,
sourceMapUrl: sourceMapPath,
});
if (isOptimizedMode) {
generateBytecodeBundle({
sourcePath: testJSBundlePath,
bytecodePath: testBytecodeBundlePath,
isOptimizedMode,
});
}
const rnTesterCommandArgs = [
const rnTesterCommandResult = runBuck2([
'run',
getBuckModeForPlatform(),
getBuckModeForPlatform(isOptimizedMode),
'//xplat/ReactNative/react-native-cxx/samples/tester:tester',
'--',
'--bundlePath',
testBundlePath,
];
const rnTesterCommandResult = spawnSync('buck2', rnTesterCommandArgs, {
encoding: 'utf8',
env: {
...process.env,
PATH: `/usr/local/bin:${process.env.PATH ?? ''}`,
},
});
]);
if (rnTesterCommandResult.status !== 0) {
throw new Error(
[
'Failed to run test in RN tester binary. Full output:',
'buck2 ' + rnTesterCommandArgs.join(' '),
'stdout:',
rnTesterCommandResult.stdout,
'stderr:',
rnTesterCommandResult.stderr,
'error:',
rnTesterCommandResult.error,
].join('\n'),
);
throw new Error(getDebugInfoFromCommandResult(rnTesterCommandResult));
}
if (PRINT_FANTOM_OUTPUT) {
console.log(
[
'RN tester binary. Full output:',
'buck2 ' + rnTesterCommandArgs.join(' '),
'stdout:',
rnTesterCommandResult.stdout,
'stderr:',
rnTesterCommandResult.stderr,
'error:',
rnTesterCommandResult.error,
].join('\n'),
);
console.log(getDebugInfoFromCommandResult(rnTesterCommandResult));
}
const rnTesterParsedOutput = parseRNTesterCommandResult(
rnTesterCommandArgs,
rnTesterCommandResult,
);
const testResultError = rnTesterParsedOutput.testResult.error;
if (testResultError) {
const error = new Error(testResultError.message);
error.stack = testResultError.stack;
error.stack = symbolicateStackTrace(sourceMapPath, testResultError.stack);
throw error;
}
@@ -248,6 +184,9 @@ module.exports = async function runTest(
failureDetails: [] as Array<string>,
testFilePath: testPath,
...testResult,
failureMessages: testResult.failureMessages.map(maybeStackTrace =>
symbolicateStackTrace(sourceMapPath, maybeStackTrace),
),
})) ?? [];
return {
+158
View File
@@ -0,0 +1,158 @@
/**
* 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 {spawnSync} from 'child_process';
import crypto from 'crypto';
import fs from 'fs';
// $FlowExpectedError[untyped-import]
import {extract, parse} from 'jest-docblock';
import os from 'os';
// $FlowExpectedError[untyped-import]
import {SourceMapConsumer} from 'source-map';
type DocblockPragmas = {[key: string]: string | string[]};
type FantomTestMode = 'dev' | 'opt';
type FantomTestConfig = {
mode: FantomTestMode,
};
const DEFAULT_MODE: FantomTestMode = 'dev';
/**
* Extracts the Fantom configuration from the test file, specified as part of
* the docblock comment. E.g.:
*
* ```
* /**
* * @flow strict-local
* * @fantom mode:opt
* *
* ```
*
* So far the only supported option is `mode`, which can be 'dev' or 'opt'.
*/
export function getFantomTestConfig(testPath: string): FantomTestConfig {
const docblock = extract(fs.readFileSync(testPath, 'utf8'));
const pragmas = parse(docblock) as DocblockPragmas;
const config = {
mode: DEFAULT_MODE,
};
const maybeMode = pragmas.fantom_mode;
if (maybeMode != null) {
if (Array.isArray(maybeMode)) {
throw new Error('Expected a single value for @fantom_mode');
}
const mode = maybeMode;
if (mode === 'dev' || mode === 'opt') {
config.mode = mode;
} else {
throw new Error(`Invalid Fantom mode: ${mode}`);
}
}
return config;
}
export function getBuckModeForPlatform(enableRelease: boolean = false): string {
const mode = enableRelease ? 'opt' : 'dev';
switch (os.platform()) {
case 'linux':
return `@//arvr/mode/linux/${mode}`;
case 'darwin':
return os.arch() === 'arm64'
? `@//arvr/mode/mac-arm/${mode}`
: `@//arvr/mode/mac/${mode}`;
case 'win32':
return `@//arvr/mode/win/${mode}`;
default:
throw new Error(`Unsupported platform: ${os.platform()}`);
}
}
type SpawnResultWithOriginalCommand = {
...ReturnType<typeof spawnSync>,
originalCommand: string,
...
};
export function runBuck2(args: Array<string>): SpawnResultWithOriginalCommand {
const result = spawnSync('buck2', args, {
encoding: 'utf8',
env: {
...process.env,
PATH: `/usr/local/bin:${process.env.PATH ?? ''}`,
},
});
return {
...result,
originalCommand: `buck2 ${args.join(' ')}`,
};
}
export function getDebugInfoFromCommandResult(
commandResult: SpawnResultWithOriginalCommand,
): string {
const logLines = [
`Command ${commandResult.status === 0 ? 'succeeded' : 'failed'}: ${commandResult.originalCommand}`,
'',
'stdout:',
commandResult.stdout,
'',
'stderr:',
commandResult.stderr,
];
if (commandResult.error) {
logLines.push('', 'error:', String(commandResult.error));
}
return logLines.join('\n');
}
export function getShortHash(contents: string): string {
return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8);
}
export function symbolicateStackTrace(
sourceMapPath: string,
stackTrace: string,
): string {
const sourceMapData = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8'));
const consumer = new SourceMapConsumer(sourceMapData);
return stackTrace
.split('\n')
.map(line => {
const match = line.match(/at (.*) \((.*):(\d+):(\d+)\)/);
if (match) {
const functionName = match[1];
// const fileName = match[2];
const lineNumber = parseInt(match[3], 10);
const columnNumber = parseInt(match[4], 10);
// Get the original position
const originalPosition = consumer.originalPositionFor({
line: lineNumber,
column: columnNumber,
});
return `at ${originalPosition.name ?? functionName} (${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`;
} else {
return line;
}
})
.join('\n');
}
+13
View File
@@ -0,0 +1,13 @@
/**
* 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
* @oncall react_native
*/
require('../../../../scripts/build/babel-register').registerForMonorepo();
module.exports = require('./warmup');
+97
View File
@@ -0,0 +1,97 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import {
getBuckModeForPlatform,
getDebugInfoFromCommandResult,
runBuck2,
} from '../utils';
// $FlowExpectedError[untyped-import]
import fs from 'fs';
import Metro from 'metro';
import os from 'os';
import path from 'path';
export default async function warmUp(
globalConfig: {...},
projectConfig: {...},
): Promise<void> {
try {
warmUpHermesCompiler();
warmUpRNTesterCLI();
await warmUpMetro();
} 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',
e,
);
}
}
}
async function warmUpMetro(): Promise<void> {
const metroConfig = await Metro.loadConfig({
config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'),
});
const entrypointPath = path.resolve(
__dirname,
'..',
'..',
'runtime',
'WarmUpEntryPoint.js',
);
const bundlePath = path.join(
os.tmpdir(),
`fantom-warmup-bundle-${Date.now()}.js`,
);
await Metro.runBuild(metroConfig, {
entry: entrypointPath,
out: bundlePath,
platform: 'android',
minify: false,
dev: true,
});
try {
fs.unlinkSync(bundlePath);
} catch {}
}
function warmUpHermesCompiler(): void {
const buildHermesCompilerCommandResult = runBuck2([
'build',
getBuckModeForPlatform(),
'//xplat/hermes/tools/hermesc:hermesc',
]);
if (buildHermesCompilerCommandResult.status !== 0) {
throw new Error(
getDebugInfoFromCommandResult(buildHermesCompilerCommandResult),
);
}
}
function warmUpRNTesterCLI(): void {
const buildRNTesterCommandResult = runBuck2([
'build',
getBuckModeForPlatform(),
'//xplat/ReactNative/react-native-cxx/samples/tester:tester',
]);
if (buildRNTesterCommandResult.status !== 0) {
throw new Error(getDebugInfoFromCommandResult(buildRNTesterCommandResult));
}
}
@@ -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.
*
* @format
* @oncall react_native
*/
/**
* This is just an entrypoint to warm up the Metro cache before the tests run.
*/
import 'react-native/Libraries/Core/InitializeCore.js';
import 'react-native/src/private/__tests__/ReactNativeTester';
import './setup';
+42
View File
@@ -307,6 +307,48 @@ class Expect {
}
}
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;
}
+1
View File
@@ -83,6 +83,7 @@
"hermes-transform": "0.25.1",
"inquirer": "^7.1.0",
"jest": "^29.6.3",
"jest-diff": "^29.7.0",
"jest-junit": "^10.0.0",
"jscodeshift": "^0.14.0",
"metro-babel-register": "^0.81.0",
+3 -3
View File
@@ -1,9 +1,9 @@
@generated SignedSource<<6b92b66e59525cef52902139f863f175>>
Git revision: b61aae3ccc6e2684dfbf1e2a06b0f985b459f11f
@generated SignedSource<<d09e665db66f49ff47c245adf0a4b543>>
Git revision: 6b80704fd50ea0bf10f5f5da5a4343de29aff8b2
Built with --nohooks: false
Is local checkout: false
Remote URL: https://github.com/facebookexperimental/rn-chrome-devtools-frontend
Remote branch: main
Remote branch: 0.77-stable
GN build args (overrides only):
is_official_build = true
Git status in checkout:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -42,7 +42,7 @@ import*as e from"../../ui/legacy/legacy.js";import*as t from"../../core/host/hos
<p>${d(a.docsDebuggingBasicsDetail)}</p>
</div>
</button>
<button class="rn-welcome-docsfeed-item" type="button" role="link" @click=${this.#o.bind(this,"https://reactnative.dev/docs/react-devtools")} title=${d(a.docsReactNativeDevTools)}>
<button class="rn-welcome-docsfeed-item" type="button" role="link" @click=${this.#o.bind(this,"https://reactnative.dev/docs/react-native-devtools")} title=${d(a.docsReactNativeDevTools)}>
<div class="rn-welcome-image" style="background-image: url('${c}')"></div>
<div>
<p class="devtools-link">${d(a.docsReactNativeDevTools)}</p>
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -12,6 +12,7 @@ import com.facebook.react.utils.detectOSAwareHermesCommand
import com.facebook.react.utils.moveTo
import com.facebook.react.utils.windowsAwareCommandLine
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.DirectoryProperty
@@ -19,6 +20,7 @@ import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
abstract class BundleHermesCTask : DefaultTask() {
@@ -26,6 +28,8 @@ abstract class BundleHermesCTask : DefaultTask() {
group = "react"
}
@get:Inject abstract val execOperations: ExecOperations
@get:Internal abstract val root: DirectoryProperty
@get:InputFiles
@@ -127,9 +131,9 @@ abstract class BundleHermesCTask : DefaultTask() {
File(jsIntermediateSourceMapsDir.get().asFile, "$bundleAssetName.compiler.map")
private fun runCommand(command: List<Any>) {
project.exec {
it.workingDir(root.get().asFile)
it.commandLine(command)
execOperations.exec { exec ->
exec.workingDir(root.get().asFile)
exec.commandLine(command)
}
}
@@ -39,7 +39,7 @@ abstract class BuildCodegenCLITask : Exec() {
}
override fun exec() {
val logfile = "${project.layout.buildDirectory}/build-cli.log"
val logfile = "${project.layout.buildDirectory.getAsFile().get()}/build-cli.log"
File(logfile).apply {
parentFile.mkdirs()
if (exists()) {
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -106,17 +106,17 @@ export function allowTransformProp(prop: string): void {
}
export function isSupportedColorStyleProp(prop: string): boolean {
return Object.hasOwn(SUPPORTED_COLOR_STYLES, prop);
return SUPPORTED_COLOR_STYLES.hasOwnProperty(prop);
}
export function isSupportedInterpolationParam(param: string): boolean {
return Object.hasOwn(SUPPORTED_INTERPOLATION_PARAMS, param);
return SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(param);
}
export function isSupportedStyleProp(prop: string): boolean {
return Object.hasOwn(SUPPORTED_STYLES, prop);
return SUPPORTED_STYLES.hasOwnProperty(prop);
}
export function isSupportedTransformProp(prop: string): boolean {
return Object.hasOwn(SUPPORTED_TRANSFORMS, prop);
return SUPPORTED_TRANSFORMS.hasOwnProperty(prop);
}
@@ -37,7 +37,7 @@ function createAnimatedProps(
const key = keys[ii];
const value = inputProps[key];
if (allowlist == null || Object.hasOwn(allowlist, key)) {
if (allowlist == null || hasOwn(allowlist, key)) {
let node;
if (key === 'style') {
node = AnimatedStyle.from(value, allowlist?.style);
@@ -271,3 +271,11 @@ export default class AnimatedProps extends AnimatedNode {
};
}
}
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowIgnore[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowIgnore[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
@@ -35,7 +35,7 @@ function createAnimatedStyle(
const key = keys[ii];
const value = inputStyle[key];
if (allowlist == null || Object.hasOwn(allowlist, key)) {
if (allowlist == null || hasOwn(allowlist, key)) {
let node;
if (value != null && key === 'transform') {
node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
@@ -241,3 +241,11 @@ export default class AnimatedStyle extends AnimatedWithChildren {
};
}
}
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowIgnore[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowIgnore[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
+11 -3
View File
@@ -28,6 +28,15 @@ type FormDataPart =
...
};
/**
* Encode a FormData filename compliant with RFC 2183
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives
*/
function encodeFilename(filename: string): string {
return encodeURIComponent(filename.replace(/\//g, '_'));
}
/**
* Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests
* with mixed data (string, native files) to be submitted via XMLHttpRequest.
@@ -82,9 +91,8 @@ class FormData {
// content type (cf. web Blob interface.)
if (typeof value === 'object' && !Array.isArray(value) && value) {
if (typeof value.name === 'string') {
headers['content-disposition'] += `; filename="${
value.name
}"; filename*=utf-8''${encodeURI(value.name)}`;
headers['content-disposition'] +=
`; filename="${encodeFilename(value.name)}"`;
}
if (typeof value.type === 'string') {
headers['content-type'] = value.type;
@@ -48,8 +48,7 @@ describe('FormData', function () {
type: 'image/jpeg',
name: 'photo.jpg',
headers: {
'content-disposition':
'form-data; name="photo"; filename="photo.jpg"; filename*=utf-8\'\'photo.jpg',
'content-disposition': 'form-data; name="photo"; filename="photo.jpg"',
'content-type': 'image/jpeg',
},
fieldName: 'photo',
@@ -70,7 +69,7 @@ describe('FormData', function () {
name: '测试photo.jpg',
headers: {
'content-disposition':
'form-data; name="photo"; filename="测试photo.jpg"; filename*=utf-8\'\'%E6%B5%8B%E8%AF%95photo.jpg',
'form-data; name="photo"; filename="%E6%B5%8B%E8%AF%95photo.jpg"',
'content-type': 'image/jpeg',
},
fieldName: 'photo',
@@ -0,0 +1,14 @@
/**
* 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 setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests';
setUpReactFabricPublicInstanceFantomTests({isModern: false});
@@ -0,0 +1,15 @@
/**
* 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 './setUpFeatureFlags';
import setUpReactFabricPublicInstanceFantomTests from './setUpReactFabricPublicInstanceFantomTests';
setUpReactFabricPublicInstanceFantomTests({isModern: true});
@@ -0,0 +1,16 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import * as ReactNativeFeatureFlags from '../../../../src/private/featureflags/ReactNativeFeatureFlags';
ReactNativeFeatureFlags.override({
enableAccessToHostTreeInFabric: () => true,
});
@@ -0,0 +1,365 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import '../../../Core/InitializeCore.js';
import * as ReactNativeTester from '../../../../src/private/__tests__/ReactNativeTester';
import 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 nullthrows from 'nullthrows';
import * as React from 'react';
export default function setUpTests({isModern}: {isModern: boolean}) {
// TODO: move these tests to the test file for `ReactNativeElement` when the legacy implementation is removed.
describe(`ReactFabricPublicInstance (${isModern ? 'modern' : 'legacy'})`, () => {
it('should provide instances of the right class as refs in host components', () => {
let node;
const root = ReactNativeTester.createRoot();
ReactNativeTester.runTask(() => {
root.render(
<View
ref={receivedNode => {
node = receivedNode;
}}
/>,
);
});
expect(node).toBeInstanceOf(
isModern ? ReactNativeElement : ReactFabricHostComponent,
);
});
describe('blur', () => {
test('blur() invokes TextInputState', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
const blurTextInput = jest.fn();
// We don't support view commands in Fantom yet, so we have to mock this.
TextInputState.blurTextInput = blurTextInput;
ReactNativeTester.runTask(() => {
node.blur();
});
expect(blurTextInput).toHaveBeenCalledTimes(1);
expect(blurTextInput.mock.calls).toEqual([[node]]);
});
});
describe('focus', () => {
test('focus() invokes TextInputState', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
const focusTextInput = jest.fn();
// We don't support view commands in Fantom yet, so we have to mock this.
TextInputState.focusTextInput = focusTextInput;
ReactNativeTester.runTask(() => {
node.focus();
});
expect(focusTextInput).toHaveBeenCalledTimes(1);
expect(focusTextInput.mock.calls).toEqual([[node]]);
});
});
describe('measure', () => {
it('component.measure(...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
const callback = jest.fn();
node.measure(callback);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 10, 10]]);
});
it('unmounted.measure(...) does nothing', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
ReactNativeTester.runTask(() => {
root.render(<></>);
});
const callback = jest.fn();
node.measure(callback);
expect(callback).not.toHaveBeenCalled();
});
});
describe('measureInWindow', () => {
it('component.measureInWindow(...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
const callback = jest.fn();
node.measureInWindow(callback);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]);
});
it('unmounted.measureInWindow(...) does nothing', () => {
const root = ReactNativeTester.createRoot();
let maybeNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeNode = node;
}}
/>,
);
});
const node = nullthrows(maybeNode);
ReactNativeTester.runTask(() => {
root.render(<></>);
});
const callback = jest.fn();
node.measureInWindow(callback);
expect(callback).not.toHaveBeenCalled();
});
});
describe('measureLayout', () => {
it('component.measureLayout(component, ...) invokes callback', () => {
const root = ReactNativeTester.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeParentNode = node;
}}>
<View
style={{width: 10, height: 10, left: 20, top: 20}}
ref={node => {
maybeChildNode = node;
}}
/>
</View>,
);
});
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
const callback = jest.fn();
childNode.measureLayout(parentNode, callback);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[20, 20, 10, 10]]);
});
it('unmounted.measureLayout(component, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeParentNode = node;
}}>
<View
style={{width: 10, height: 10, left: 20, top: 20}}
ref={node => {
maybeChildNode = node;
}}
/>
</View>,
);
});
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
root.render(
<View style={{width: 100, height: 100, left: 10, top: 10}} />,
);
});
const callback = jest.fn();
childNode.measureLayout(parentNode, callback);
expect(callback).not.toHaveBeenCalled();
});
it('component.measureLayout(unmounted, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeParentNode = node;
}}>
<View
style={{width: 10, height: 10, left: 20, top: 20}}
ref={node => {
maybeChildNode = node;
}}
/>
</View>,
);
});
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
root.render(
<View style={{width: 100, height: 100, left: 10, top: 10}} />,
);
});
const callback = jest.fn();
parentNode.measureLayout(childNode, callback);
expect(callback).not.toHaveBeenCalled();
});
it('unmounted.measureLayout(unmounted, ...) does nothing', () => {
const root = ReactNativeTester.createRoot();
let maybeParentNode;
let maybeChildNode;
ReactNativeTester.runTask(() => {
root.render(
<View
style={{width: 100, height: 100, left: 10, top: 10}}
ref={node => {
maybeParentNode = node;
}}>
<View
style={{width: 10, height: 10, left: 20, top: 20}}
ref={node => {
maybeChildNode = node;
}}
/>
</View>,
);
});
const parentNode = nullthrows(maybeParentNode);
const childNode = nullthrows(maybeChildNode);
ReactNativeTester.runTask(() => {
root.render(<></>);
});
const callback = jest.fn();
childNode.measureLayout(parentNode, callback);
expect(callback).not.toHaveBeenCalled();
});
});
});
}
@@ -10,7 +10,7 @@
/**
* UIView class for root <ModalHostView> component.
*/
@interface RCTModalHostViewComponentView : RCTViewComponentView
@interface RCTModalHostViewComponentView : RCTViewComponentView <UIAdaptivePresentationControllerDelegate>
/**
* Subclasses may override this method and present the modal on different view controller.
@@ -149,6 +149,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
{
BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window;
if (shouldBePresented) {
self.viewController.presentationController.delegate = self;
_isPresented = YES;
[self presentViewController:self.viewController
animated:_shouldAnimatePresentation
@@ -274,6 +276,16 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
[childComponentView removeFromSuperview];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)controller
{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onRequestClose({});
}
}
@end
#ifdef __cplusplus
@@ -426,6 +426,10 @@ public abstract interface class com/facebook/react/ReactRootView$ReactRootViewEv
public abstract fun onAttachedToReactInstance (Lcom/facebook/react/ReactRootView;)V
}
public abstract class com/facebook/react/TurboReactPackage : com/facebook/react/BaseReactPackage {
public fun <init> ()V
}
public abstract interface class com/facebook/react/ViewManagerOnDemandReactPackage {
public abstract fun createViewManager (Lcom/facebook/react/bridge/ReactApplicationContext;Ljava/lang/String;)Lcom/facebook/react/uimanager/ViewManager;
public abstract fun getViewManagerNames (Lcom/facebook/react/bridge/ReactApplicationContext;)Ljava/util/Collection;
@@ -7137,45 +7141,6 @@ public final class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout
public fun setRefreshing (Z)V
}
public class com/facebook/react/views/switchview/ReactSwitchManager : com/facebook/react/uimanager/SimpleViewManager, com/facebook/react/viewmanagers/AndroidSwitchManagerInterface {
public static final field REACT_CLASS Ljava/lang/String;
public fun <init> ()V
protected synthetic fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V
protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/switchview/ReactSwitch;)V
public fun createShadowNodeInstance ()Lcom/facebook/react/uimanager/LayoutShadowNode;
public synthetic fun createShadowNodeInstance ()Lcom/facebook/react/uimanager/ReactShadowNode;
protected synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View;
protected fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/switchview/ReactSwitch;
protected fun getDelegate ()Lcom/facebook/react/uimanager/ViewManagerDelegate;
public fun getName ()Ljava/lang/String;
public fun getShadowNodeClass ()Ljava/lang/Class;
public fun measure (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;[F)J
public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public fun receiveCommand (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public synthetic fun setBackgroundColor (Landroid/view/View;I)V
public fun setBackgroundColor (Lcom/facebook/react/views/switchview/ReactSwitch;I)V
public synthetic fun setDisabled (Landroid/view/View;Z)V
public fun setDisabled (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V
public synthetic fun setEnabled (Landroid/view/View;Z)V
public fun setEnabled (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V
public synthetic fun setNativeValue (Landroid/view/View;Z)V
public fun setNativeValue (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V
public synthetic fun setOn (Landroid/view/View;Z)V
public fun setOn (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V
public synthetic fun setThumbColor (Landroid/view/View;Ljava/lang/Integer;)V
public fun setThumbColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V
public synthetic fun setThumbTintColor (Landroid/view/View;Ljava/lang/Integer;)V
public fun setThumbTintColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V
public synthetic fun setTrackColorForFalse (Landroid/view/View;Ljava/lang/Integer;)V
public fun setTrackColorForFalse (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V
public synthetic fun setTrackColorForTrue (Landroid/view/View;Ljava/lang/Integer;)V
public fun setTrackColorForTrue (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V
public synthetic fun setTrackTintColor (Landroid/view/View;Ljava/lang/Integer;)V
public fun setTrackTintColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V
public synthetic fun setValue (Landroid/view/View;Z)V
public fun setValue (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V
}
public final class com/facebook/react/views/text/DefaultStyleValuesUtil {
public static final field INSTANCE Lcom/facebook/react/views/text/DefaultStyleValuesUtil;
public static final fun getDefaultTextColor (Landroid/content/Context;)Landroid/content/res/ColorStateList;
@@ -0,0 +1,13 @@
/*
* 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
@Deprecated(
message = "Use BaseReactPackage instead",
replaceWith = ReplaceWith(expression = "BaseReactPackage"))
public abstract class TurboReactPackage : BaseReactPackage() {}
@@ -445,12 +445,18 @@ public class FabricUIManager
@Override
public void markActiveTouchForTag(int surfaceId, int reactTag) {
mMountingManager.getSurfaceManager(surfaceId).markActiveTouchForTag(reactTag);
SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId);
if (surfaceMountingManager != null) {
surfaceMountingManager.markActiveTouchForTag(reactTag);
}
}
@Override
public void sweepActiveTouchForTag(int surfaceId, int reactTag) {
mMountingManager.getSurfaceManager(surfaceId).sweepActiveTouchForTag(reactTag);
SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId);
if (surfaceMountingManager != null) {
surfaceMountingManager.sweepActiveTouchForTag(reactTag);
}
}
/**
@@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatDelegate
import com.facebook.fbreact.specs.NativeAppearanceSpec
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
/** Module that exposes the user's preferred color scheme. */
@@ -58,11 +59,13 @@ constructor(
}
public override fun setColorScheme(style: String) {
when (style) {
"dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"unspecified" ->
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
UiThreadUtil.runOnUiThread {
when (style) {
"dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"unspecified" ->
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
}
@@ -1,232 +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.
*/
// switchview because switch is a keyword
package com.facebook.react.views.switchview;
import android.content.Context;
import android.view.View;
import android.widget.CompoundButton;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.AndroidSwitchManagerDelegate;
import com.facebook.react.viewmanagers.AndroidSwitchManagerInterface;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.yoga.YogaNode;
/** View manager for {@link ReactSwitch} components. */
public class ReactSwitchManager extends SimpleViewManager<ReactSwitch>
implements AndroidSwitchManagerInterface<ReactSwitch> {
public static final String REACT_CLASS = "AndroidSwitch";
static class ReactSwitchShadowNode extends LayoutShadowNode implements YogaMeasureFunction {
private int mWidth;
private int mHeight;
private boolean mMeasured;
private ReactSwitchShadowNode() {
initMeasureFunction();
}
private void initMeasureFunction() {
setMeasureFunction(this);
}
@Override
public long measure(
YogaNode node,
float width,
YogaMeasureMode widthMode,
float height,
YogaMeasureMode heightMode) {
if (!mMeasured) {
// Create a switch with the default config and measure it; since we don't (currently)
// support setting custom switch text, this is fine, as all switches will measure the same
// on a specific device/theme/locale combination.
ReactSwitch reactSwitch = new ReactSwitch(getThemedContext());
reactSwitch.setShowText(false);
final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
reactSwitch.measure(spec, spec);
mWidth = reactSwitch.getMeasuredWidth();
mHeight = reactSwitch.getMeasuredHeight();
mMeasured = true;
}
return YogaMeasureOutput.make(mWidth, mHeight);
}
}
private static final CompoundButton.OnCheckedChangeListener ON_CHECKED_CHANGE_LISTENER =
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ReactContext reactContext = (ReactContext) buttonView.getContext();
int reactTag = buttonView.getId();
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
.dispatchEvent(
new ReactSwitchEvent(
UIManagerHelper.getSurfaceId(reactContext), reactTag, isChecked));
}
};
private final ViewManagerDelegate<ReactSwitch> mDelegate;
public ReactSwitchManager() {
mDelegate = new AndroidSwitchManagerDelegate<>(this);
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public LayoutShadowNode createShadowNodeInstance() {
return new ReactSwitchShadowNode();
}
@Override
public Class getShadowNodeClass() {
return ReactSwitchShadowNode.class;
}
@Override
protected ReactSwitch createViewInstance(ThemedReactContext context) {
ReactSwitch view = new ReactSwitch(context);
view.setShowText(false);
return view;
}
@Override
public void setBackgroundColor(ReactSwitch view, @ColorInt int backgroundColor) {
view.setBackgroundColor(backgroundColor);
}
@Override
@ReactProp(name = "disabled", defaultBoolean = false)
public void setDisabled(ReactSwitch view, boolean disabled) {
view.setEnabled(!disabled);
}
@Override
@ReactProp(name = ViewProps.ENABLED, defaultBoolean = true)
public void setEnabled(ReactSwitch view, boolean enabled) {
view.setEnabled(enabled);
}
@Override
@ReactProp(name = ViewProps.ON)
public void setOn(ReactSwitch view, boolean on) {
setValueInternal(view, on);
}
@Override
@ReactProp(name = "value")
public void setValue(ReactSwitch view, boolean value) {
setValueInternal(view, value);
}
@Override
@ReactProp(name = "thumbTintColor", customType = "Color")
public void setThumbTintColor(ReactSwitch view, @Nullable Integer color) {
this.setThumbColor(view, color);
}
@Override
@ReactProp(name = "thumbColor", customType = "Color")
public void setThumbColor(ReactSwitch view, @Nullable Integer color) {
view.setThumbColor(color);
}
@Override
@ReactProp(name = "trackColorForFalse", customType = "Color")
public void setTrackColorForFalse(ReactSwitch view, @Nullable Integer color) {
view.setTrackColorForFalse(color);
}
@Override
@ReactProp(name = "trackColorForTrue", customType = "Color")
public void setTrackColorForTrue(ReactSwitch view, @Nullable Integer color) {
view.setTrackColorForTrue(color);
}
@Override
@ReactProp(name = "trackTintColor", customType = "Color")
public void setTrackTintColor(ReactSwitch view, @Nullable Integer color) {
view.setTrackColor(color);
}
@Override
public void setNativeValue(ReactSwitch view, boolean value) {
setValueInternal(view, value);
}
@Override
public void receiveCommand(
@NonNull ReactSwitch view, String commandId, @Nullable ReadableArray args) {
switch (commandId) {
case "setNativeValue":
setValueInternal(view, args != null && args.getBoolean(0));
break;
}
}
@Override
protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSwitch view) {
view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER);
}
@Override
protected ViewManagerDelegate<ReactSwitch> getDelegate() {
return mDelegate;
}
@Override
public long measure(
Context context,
ReadableMap localData,
ReadableMap props,
ReadableMap state,
float width,
YogaMeasureMode widthMode,
float height,
YogaMeasureMode heightMode,
@Nullable float[] attachmentsPositions) {
ReactSwitch view = new ReactSwitch(context);
view.setShowText(false);
int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(measureSpec, measureSpec);
return YogaMeasureOutput.make(
PixelUtil.toDIPFromPixel(view.getMeasuredWidth()),
PixelUtil.toDIPFromPixel(view.getMeasuredHeight()));
}
private static void setValueInternal(ReactSwitch view, boolean value) {
// we set the checked change listener to null and then restore it so that we don't fire an
// onChange event to JS when JS itself is updating the value of the switch
view.setOnCheckedChangeListener(null);
view.setOn(value);
view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER);
}
}
@@ -0,0 +1,152 @@
/*
* 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.views.switchview
import android.content.Context
import android.view.View
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.BaseViewManager
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.ViewProps
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.AndroidSwitchManagerDelegate
import com.facebook.react.viewmanagers.AndroidSwitchManagerInterface
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput
internal class ReactSwitchManager :
BaseViewManager<ReactSwitch, ReactSwitchShadowNode>(),
AndroidSwitchManagerInterface<ReactSwitch> {
private val delegate: ViewManagerDelegate<ReactSwitch> = AndroidSwitchManagerDelegate(this)
override fun getName(): String = REACT_CLASS
override fun createShadowNodeInstance(): ReactSwitchShadowNode = ReactSwitchShadowNode()
override fun getShadowNodeClass(): Class<ReactSwitchShadowNode> =
ReactSwitchShadowNode::class.java
override fun createViewInstance(context: ThemedReactContext): ReactSwitch =
ReactSwitch(context).apply { showText = false }
override fun setBackgroundColor(view: ReactSwitch, @ColorInt backgroundColor: Int) {
view.setBackgroundColor(backgroundColor)
}
@ReactProp(name = "disabled", defaultBoolean = false)
override fun setDisabled(view: ReactSwitch, disabled: Boolean) {
view.isEnabled = !disabled
}
@ReactProp(name = ViewProps.ENABLED, defaultBoolean = true)
override fun setEnabled(view: ReactSwitch, enabled: Boolean) {
view.isEnabled = enabled
}
@ReactProp(name = ViewProps.ON)
override fun setOn(view: ReactSwitch, on: Boolean) {
setValueInternal(view, on)
}
@ReactProp(name = "value")
override fun setValue(view: ReactSwitch, value: Boolean) {
setValueInternal(view, value)
}
@ReactProp(name = "thumbTintColor", customType = "Color")
override fun setThumbTintColor(view: ReactSwitch, color: Int?) {
setThumbColor(view, color)
}
@ReactProp(name = "thumbColor", customType = "Color")
override fun setThumbColor(view: ReactSwitch, color: Int?) {
view.setThumbColor(color)
}
@ReactProp(name = "trackColorForFalse", customType = "Color")
override fun setTrackColorForFalse(view: ReactSwitch, color: Int?) {
view.setTrackColorForFalse(color)
}
@ReactProp(name = "trackColorForTrue", customType = "Color")
override fun setTrackColorForTrue(view: ReactSwitch, color: Int?) {
view.setTrackColorForTrue(color)
}
@ReactProp(name = "trackTintColor", customType = "Color")
override fun setTrackTintColor(view: ReactSwitch, color: Int?) {
view.setTrackColor(color)
}
override fun setNativeValue(view: ReactSwitch, value: Boolean) {
setValueInternal(view, value)
}
override fun receiveCommand(view: ReactSwitch, commandId: String, args: ReadableArray?) {
when (commandId) {
"setNativeValue" -> setValueInternal(view, args?.getBoolean(0) ?: false)
}
}
override fun addEventEmitters(reactContext: ThemedReactContext, view: ReactSwitch) {
view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER)
}
override fun updateExtraData(root: ReactSwitch, extraData: Any) {
// Do nothing
}
override fun getDelegate(): ViewManagerDelegate<ReactSwitch> = delegate
override fun measure(
context: Context,
localData: ReadableMap?,
props: ReadableMap?,
state: ReadableMap?,
width: Float,
widthMode: YogaMeasureMode,
height: Float,
heightMode: YogaMeasureMode,
attachmentsPositions: FloatArray?
): Long {
val view = ReactSwitch(context).apply { showText = false }
val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(measureSpec, measureSpec)
return YogaMeasureOutput.make(
PixelUtil.toDIPFromPixel(view.measuredWidth.toFloat()),
PixelUtil.toDIPFromPixel(view.measuredHeight.toFloat()))
}
private fun setValueInternal(view: ReactSwitch, value: Boolean) {
// Temporarily remove the listener to avoid triggering JS events
view.setOnCheckedChangeListener(null)
view.setOn(value)
view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER)
}
internal companion object {
public const val REACT_CLASS: String = "AndroidSwitch"
private val ON_CHECKED_CHANGE_LISTENER =
CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
val reactContext = buttonView.context as ReactContext
val reactTag = buttonView.id
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
?.dispatchEvent(
ReactSwitchEvent(UIManagerHelper.getSurfaceId(reactContext), reactTag, isChecked))
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.views.switchview
import android.view.View
import com.facebook.react.uimanager.LayoutShadowNode
import com.facebook.yoga.YogaMeasureFunction
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput
import com.facebook.yoga.YogaNode
internal class ReactSwitchShadowNode : LayoutShadowNode(), YogaMeasureFunction {
private var width = 0
private var height = 0
private var measured = false
init {
initMeasureFunction()
}
private fun initMeasureFunction() {
setMeasureFunction(this)
}
override fun measure(
node: YogaNode,
width: Float,
widthMode: YogaMeasureMode,
height: Float,
heightMode: YogaMeasureMode
): Long {
if (!measured) {
// Create a switch with the default config and measure it; since we don't (currently)
// support setting custom switch text, this is fine, as all switches will measure the same
// on a specific device/theme/locale combination.
val reactSwitch = ReactSwitch(themedContext)
reactSwitch.showText = false
val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
reactSwitch.measure(spec, spec)
this.width = reactSwitch.measuredWidth
this.height = reactSwitch.measuredHeight
measured = true
}
return YogaMeasureOutput.make(this.width, this.height)
}
}
@@ -221,6 +221,13 @@ Pod::Spec.new do |s|
end
s.subspec "consistency" do |ss|
ss.dependency folly_dep_name, folly_version
ss.compiler_flags = folly_compiler_flags
ss.source_files = "react/renderer/consistency/**/*.{m,mm,cpp,h}"
ss.header_dir = "react/renderer/consistency"
end
s.subspec "uimanager" do |ss|
ss.subspec "consistency" do |sss|
sss.dependency folly_dep_name, folly_version
@@ -125,18 +125,14 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
} else if (req.method == "FuseboxClient.setClientMetadata") {
} else if (req.method == "ReactNativeApplication.enable") {
sessionState_.isReactNativeApplicationDomainEnabled = true;
fuseboxClientType_ = FuseboxClientType::Fusebox;
if (sessionState_.isLogDomainEnabled) {
sendFuseboxNotice();
}
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
} else if (req.method == "ReactNativeApplication.enable") {
sessionState_.isReactNativeApplicationDomainEnabled = true;
frontendChannel_(cdp::jsonNotification(
"ReactNativeApplication.metadataUpdated",
createHostMetadataPayload(hostMetadata_)));
@@ -348,21 +348,6 @@ TYPED_TEST(JsiIntegrationPortableTest, ExceptionDuringAddBindingIsIgnored) {
EXPECT_TRUE(this->eval("globalThis.foo === 42").getBool());
}
TYPED_TEST(JsiIntegrationPortableTest, FuseboxSetClientMetadata) {
this->connect();
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "FuseboxClient.setClientMetadata",
"params": {}
})");
}
TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationEnable) {
this->connect();
@@ -41,16 +41,38 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes& rhs) const {
#if RN_DEBUG_STRING_CONVERTIBLE
SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const {
ParagraphAttributes paragraphAttributes{};
return {
debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines),
debugStringConvertibleItem("ellipsizeMode", ellipsizeMode),
debugStringConvertibleItem("textBreakStrategy", textBreakStrategy),
debugStringConvertibleItem("adjustsFontSizeToFit", adjustsFontSizeToFit),
debugStringConvertibleItem("minimumFontSize", minimumFontSize),
debugStringConvertibleItem("maximumFontSize", maximumFontSize),
debugStringConvertibleItem("includeFontPadding", includeFontPadding),
debugStringConvertibleItem(
"android_hyphenationFrequency", android_hyphenationFrequency)};
"maximumNumberOfLines",
maximumNumberOfLines,
paragraphAttributes.maximumNumberOfLines),
debugStringConvertibleItem(
"ellipsizeMode", ellipsizeMode, paragraphAttributes.ellipsizeMode),
debugStringConvertibleItem(
"textBreakStrategy",
textBreakStrategy,
paragraphAttributes.textBreakStrategy),
debugStringConvertibleItem(
"adjustsFontSizeToFit",
adjustsFontSizeToFit,
paragraphAttributes.adjustsFontSizeToFit),
debugStringConvertibleItem(
"minimumFontSize",
minimumFontSize,
paragraphAttributes.minimumFontSize),
debugStringConvertibleItem(
"maximumFontSize",
maximumFontSize,
paragraphAttributes.maximumFontSize),
debugStringConvertibleItem(
"includeFontPadding",
includeFontPadding,
paragraphAttributes.includeFontPadding),
debugStringConvertibleItem(
"android_hyphenationFrequency",
android_hyphenationFrequency,
paragraphAttributes.android_hyphenationFrequency)};
}
#endif
@@ -197,49 +197,97 @@ TextAttributes TextAttributes::defaultTextAttributes() {
#if RN_DEBUG_STRING_CONVERTIBLE
SharedDebugStringConvertibleList TextAttributes::getDebugProps() const {
const auto& textAttributes = TextAttributes::defaultTextAttributes();
return {
// Color
debugStringConvertibleItem("backgroundColor", backgroundColor),
debugStringConvertibleItem("foregroundColor", foregroundColor),
debugStringConvertibleItem("opacity", opacity),
debugStringConvertibleItem(
"backgroundColor", backgroundColor, textAttributes.backgroundColor),
debugStringConvertibleItem(
"foregroundColor", foregroundColor, textAttributes.foregroundColor),
debugStringConvertibleItem("opacity", opacity, textAttributes.opacity),
// Font
debugStringConvertibleItem("fontFamily", fontFamily),
debugStringConvertibleItem("fontSize", fontSize),
debugStringConvertibleItem("fontSizeMultiplier", fontSizeMultiplier),
debugStringConvertibleItem("fontWeight", fontWeight),
debugStringConvertibleItem("fontStyle", fontStyle),
debugStringConvertibleItem("fontVariant", fontVariant),
debugStringConvertibleItem("allowFontScaling", allowFontScaling),
debugStringConvertibleItem("dynamicTypeRamp", dynamicTypeRamp),
debugStringConvertibleItem("letterSpacing", letterSpacing),
debugStringConvertibleItem(
"fontFamily", fontFamily, textAttributes.fontFamily),
debugStringConvertibleItem("fontSize", fontSize, textAttributes.fontSize),
debugStringConvertibleItem(
"fontSizeMultiplier",
fontSizeMultiplier,
textAttributes.fontSizeMultiplier),
debugStringConvertibleItem(
"fontWeight", fontWeight, textAttributes.fontWeight),
debugStringConvertibleItem(
"fontStyle", fontStyle, textAttributes.fontStyle),
debugStringConvertibleItem(
"fontVariant", fontVariant, textAttributes.fontVariant),
debugStringConvertibleItem(
"allowFontScaling",
allowFontScaling,
textAttributes.allowFontScaling),
debugStringConvertibleItem(
"dynamicTypeRamp", dynamicTypeRamp, textAttributes.dynamicTypeRamp),
debugStringConvertibleItem(
"letterSpacing", letterSpacing, textAttributes.letterSpacing),
// Paragraph Styles
debugStringConvertibleItem("lineHeight", lineHeight),
debugStringConvertibleItem("alignment", alignment),
debugStringConvertibleItem("baseWritingDirection", baseWritingDirection),
debugStringConvertibleItem("lineBreakStrategyIOS", lineBreakStrategy),
debugStringConvertibleItem("lineBreakModeIOS", lineBreakMode),
debugStringConvertibleItem(
"lineHeight", lineHeight, textAttributes.lineHeight),
debugStringConvertibleItem(
"alignment", alignment, textAttributes.alignment),
debugStringConvertibleItem(
"baseWritingDirection",
baseWritingDirection,
textAttributes.baseWritingDirection),
debugStringConvertibleItem(
"lineBreakStrategyIOS",
lineBreakStrategy,
textAttributes.lineBreakStrategy),
debugStringConvertibleItem(
"lineBreakModeIOS", lineBreakMode, textAttributes.lineBreakMode),
// Decoration
debugStringConvertibleItem("textDecorationColor", textDecorationColor),
debugStringConvertibleItem(
"textDecorationLineType", textDecorationLineType),
debugStringConvertibleItem("textDecorationStyle", textDecorationStyle),
"textDecorationColor",
textDecorationColor,
textAttributes.textDecorationColor),
debugStringConvertibleItem(
"textDecorationLineType",
textDecorationLineType,
textAttributes.textDecorationLineType),
debugStringConvertibleItem(
"textDecorationStyle",
textDecorationStyle,
textAttributes.textDecorationStyle),
// Shadow
debugStringConvertibleItem("textShadowOffset", textShadowOffset),
debugStringConvertibleItem("textShadowRadius", textShadowRadius),
debugStringConvertibleItem("textShadowColor", textShadowColor),
debugStringConvertibleItem(
"textShadowOffset",
textShadowOffset,
textAttributes.textShadowOffset),
debugStringConvertibleItem(
"textShadowRadius",
textShadowRadius,
textAttributes.textShadowRadius),
debugStringConvertibleItem(
"textShadowColor", textShadowColor, textAttributes.textShadowColor),
// Special
debugStringConvertibleItem("isHighlighted", isHighlighted),
debugStringConvertibleItem("isPressable", isPressable),
debugStringConvertibleItem("layoutDirection", layoutDirection),
debugStringConvertibleItem("accessibilityRole", accessibilityRole),
debugStringConvertibleItem("role", role),
debugStringConvertibleItem(
"isHighlighted", isHighlighted, textAttributes.isHighlighted),
debugStringConvertibleItem(
"isPressable", isPressable, textAttributes.isPressable),
debugStringConvertibleItem(
"layoutDirection", layoutDirection, textAttributes.layoutDirection),
debugStringConvertibleItem(
"accessibilityRole",
accessibilityRole,
textAttributes.accessibilityRole),
debugStringConvertibleItem("role", role, textAttributes.role),
debugStringConvertibleItem("textAlignVertical", textAlignVertical),
debugStringConvertibleItem(
"textAlignVertical",
textAlignVertical,
textAttributes.textAlignVertical),
};
}
#endif
@@ -15,14 +15,29 @@
#include <react/renderer/debug/DebugStringConvertible.h>
#include <react/renderer/debug/DebugStringConvertibleItem.h>
#include <react/utils/FloatComparison.h>
namespace facebook::react {
#if RN_DEBUG_STRING_CONVERTIBLE
inline SharedDebugStringConvertible debugStringConvertibleItem(
const std::string& name,
float value,
float defaultValue = {}) {
if (floatEquality(value, defaultValue)) {
return nullptr;
}
return std::make_shared<DebugStringConvertibleItem>(
name, facebook::react::toString(value));
}
template <typename T>
inline SharedDebugStringConvertible
debugStringConvertibleItem(std::string name, T value, T defaultValue = {}) {
inline SharedDebugStringConvertible debugStringConvertibleItem(
const std::string& name,
T value,
T defaultValue = {}) {
if (value == defaultValue) {
return nullptr;
}
@@ -33,7 +48,7 @@ debugStringConvertibleItem(std::string name, T value, T defaultValue = {}) {
template <typename T>
inline SharedDebugStringConvertible debugStringConvertibleItem(
std::string name,
const std::string& name,
std::optional<T> value,
T defaultValue = {}) {
if (!value.has_value()) {
@@ -54,7 +69,7 @@ inline SharedDebugStringConvertibleList operator+(
}
inline SharedDebugStringConvertible debugStringConvertibleItem(
std::string name,
const std::string& name,
DebugStringConvertible value,
std::string defaultValue) {
return debugStringConvertibleItem(
@@ -45,17 +45,22 @@ static SharedViewProps nonFlattenedDefaultProps(
static ShadowNode::Shared makeNode(
const ComponentDescriptor& componentDescriptor,
int tag,
const ShadowNode::ListOfShared& children,
std::shared_ptr<ShadowNode::ListOfShared> children,
bool flattened = false) {
auto props = flattened ? generateDefaultProps(componentDescriptor)
: nonFlattenedDefaultProps(componentDescriptor);
return componentDescriptor.createShadowNode(
ShadowNodeFragment{
props, std::make_shared<ShadowNode::ListOfShared>(children)},
ShadowNodeFragment{std::move(props), std::move(children)},
componentDescriptor.createFamily({tag, SurfaceId(1), nullptr}));
}
static std::shared_ptr<ShadowNode::ListOfShared> listOfChildren(
std::initializer_list<ShadowNode::Shared> list) {
return std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{list});
}
/**
* Test reordering of views with the same parent:
*
@@ -117,25 +122,20 @@ TEST(MountingTest, testReorderingInstructionGeneration) {
auto shadowNodeV1 = viewComponentDescriptor.createShadowNode(
ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childC, childD})},
listOfChildren({childB, childC, childD})},
family);
auto shadowNodeV2 = shadowNodeV1->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childA, childB, childC, childD})});
listOfChildren({childA, childB, childC, childD})});
auto shadowNodeV3 = shadowNodeV2->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childC, childD})});
listOfChildren({childB, childC, childD})});
auto shadowNodeV4 = shadowNodeV3->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childD, childE})});
listOfChildren({childB, childD, childE})});
auto shadowNodeV5 = shadowNodeV4->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childB, childA, childE, childC})});
listOfChildren({childB, childA, childE, childC})});
auto shadowNodeV6 = shadowNodeV5->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
@@ -157,38 +157,31 @@ TEST(MountingTest, testReorderingInstructionGeneration) {
auto rootNodeV1 = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV1})}));
listOfChildren({shadowNodeV1})}));
auto rootNodeV2 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV1->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV2})}));
listOfChildren({shadowNodeV2})}));
auto rootNodeV3 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV2->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV3})}));
listOfChildren({shadowNodeV3})}));
auto rootNodeV4 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV3->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV4})}));
listOfChildren({shadowNodeV4})}));
auto rootNodeV5 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV4->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV5})}));
listOfChildren({shadowNodeV5})}));
auto rootNodeV6 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV5->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV6})}));
listOfChildren({shadowNodeV6})}));
auto rootNodeV7 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV6->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV7})}));
listOfChildren({shadowNodeV7})}));
// Layout
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV1{};
@@ -289,7 +282,6 @@ TEST(MountingTest, testReorderingInstructionGeneration) {
// Calculating mutations.
auto mutations3 = calculateShadowViewMutations(*rootNodeV3, *rootNodeV4);
LOG(ERROR) << "Num mutations IN OLD TEST mutations3: " << mutations3.size();
// The order and exact mutation instructions here may change at any time.
// This test just ensures that any changes are intentional.
@@ -425,172 +417,107 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
auto family =
viewComponentDescriptor.createFamily({10, SurfaceId(1), nullptr});
auto reparentedViewA = makeNode(
viewComponentDescriptor,
1000,
ShadowNode::ListOfShared{
childC->clone({}), childA->clone({}), childB->clone({})});
auto reparentedViewA_ = makeNode(
viewComponentDescriptor, 1000, listOfChildren({childC, childA, childB}));
auto reparentedViewA = reparentedViewA_->clone(
ShadowNodeFragment{nonFlattenedDefaultProps(viewComponentDescriptor)});
auto reparentedViewB = makeNode(
viewComponentDescriptor,
2000,
ShadowNode::ListOfShared{
childF->clone({}), childE->clone({}), childD->clone({})});
viewComponentDescriptor, 2000, listOfChildren({childF, childE, childD}));
// Root -> G* -> H -> I -> J -> A* [nodes with * are _not_ flattened]
auto shadowNodeV1 = viewComponentDescriptor.createShadowNode(
ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
listOfChildren({childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childH->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({childJ->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})},
listOfChildren({reparentedViewA_})})})})})})})})})},
family);
// Root -> G* -> H* -> I -> J -> A* [nodes with * are _not_ flattened]
// Force an update with A with new props
auto shadowNodeV2 = shadowNodeV1->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
listOfChildren({childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})});
listOfChildren({childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({childJ->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({reparentedViewA})})})})})})})})})});
// Root -> G* -> H -> I -> J -> A* [nodes with * are _not_ flattened]
auto shadowNodeV3 = shadowNodeV2->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
listOfChildren({childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childH->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren({childJ->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone(
{})})})})})})})})})})});
listOfChildren({reparentedViewA})})})})})})})})})});
// The view is reparented 1 level down with a different sibling
// Root -> G* -> H* -> I* -> J -> [B*, A*] [nodes with * are _not_ flattened]
auto shadowNodeV4 = shadowNodeV3->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
listOfChildren({childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
generateDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewB->clone({}),
reparentedViewA->clone(
{})})})})})})})})})})});
listOfChildren({childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childJ->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
listOfChildren(
{reparentedViewB, reparentedViewA})})})})})})})})})});
// The view is reparented 1 level further down with its order with the sibling
// swapped
// Root -> G* -> H* -> I* -> J* -> [A*, B*] [nodes with * are _not_ flattened]
auto shadowNodeV5 = shadowNodeV4->clone(ShadowNodeFragment{
generateDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childG->clone(ShadowNodeFragment{
listOfChildren({childG->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childH->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{childH->clone(ShadowNodeFragment{
listOfChildren({childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
listOfChildren({childJ->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(ShadowNode::ListOfShared{
childI->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(viewComponentDescriptor),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
childJ->clone(ShadowNodeFragment{
nonFlattenedDefaultProps(
viewComponentDescriptor),
std::make_shared<
ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{
reparentedViewA->clone({}),
reparentedViewB->clone(
{})})})})})})})})})})});
listOfChildren(
{reparentedViewA, reparentedViewB})})})})})})})})})});
// Injecting a tree into the root node.
auto rootNodeV1 = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV1})}));
listOfChildren({shadowNodeV1})}));
auto rootNodeV2 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV1->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV2})}));
listOfChildren({shadowNodeV2})}));
auto rootNodeV3 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV2->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV3})}));
listOfChildren({shadowNodeV3})}));
auto rootNodeV4 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV3->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV4})}));
listOfChildren({shadowNodeV4})}));
auto rootNodeV5 = std::static_pointer_cast<const RootShadowNode>(
rootNodeV4->ShadowNode::clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),
std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared{shadowNodeV5})}));
listOfChildren({shadowNodeV5})}));
// Layout
std::vector<const LayoutableShadowNode*> affectedLayoutableNodesV1{};
@@ -626,33 +553,38 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
// Calculating mutations.
auto mutations1 = calculateShadowViewMutations(*rootNodeV1, *rootNodeV2);
EXPECT_EQ(mutations1.size(), 5);
EXPECT_EQ(mutations1.size(), 6);
EXPECT_EQ(mutations1[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations1[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations1[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations1[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations1[2].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations1[2].newChildShadowView.tag, 107);
EXPECT_EQ(mutations1[3].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations1[3].newChildShadowView.tag, 107);
EXPECT_EQ(mutations1[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations1[1].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations1[1].oldChildShadowView.tag, reparentedViewA->getTag());
// This is incorrect! ChildH does not exist yet at this point
EXPECT_EQ(mutations1[1].parentShadowView.tag, childH->getTag());
EXPECT_EQ(mutations1[2].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations1[2].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations1[3].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations1[3].newChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations1[4].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations1[4].newChildShadowView.tag, 1000);
EXPECT_EQ(mutations1[4].newChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations1[5].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations1[5].newChildShadowView.tag, reparentedViewA->getTag());
auto mutations2 = calculateShadowViewMutations(*rootNodeV2, *rootNodeV3);
EXPECT_EQ(mutations2.size(), 5);
EXPECT_EQ(mutations2[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations2[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations2[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations2[0].parentShadowView.tag, emptyRootNode->getTag());
EXPECT_EQ(mutations2[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations2[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations2[1].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations2[2].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations2[2].oldChildShadowView.tag, 107);
EXPECT_EQ(mutations2[2].oldChildShadowView.tag, childH->getTag());
EXPECT_EQ(
mutations2[3].type,
ShadowViewMutation::Delete); // correct, 107 is removed from tree entirely
EXPECT_EQ(mutations2[3].oldChildShadowView.tag, 107);
ShadowViewMutation::Delete); // correct, H is removed from tree entirely
EXPECT_EQ(mutations2[3].oldChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations2[4].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations2[4].newChildShadowView.tag, 1000);
EXPECT_EQ(mutations2[4].newChildShadowView.tag, reparentedViewA->getTag());
auto mutations3 = calculateShadowViewMutations(*rootNodeV3, *rootNodeV4);
@@ -661,57 +593,58 @@ TEST(MountingTest, testViewReparentingInstructionGeneration) {
EXPECT_EQ(mutations3.size(), 15);
EXPECT_EQ(mutations3[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations3[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations3[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations3[0].parentShadowView.tag, emptyRootNode->getTag());
EXPECT_EQ(mutations3[1].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations3[1].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations3[1].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations3[2].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[2].newChildShadowView.tag, 107);
EXPECT_EQ(mutations3[2].newChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations3[3].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[3].newChildShadowView.tag, 2000);
EXPECT_EQ(mutations3[3].newChildShadowView.tag, reparentedViewB->getTag());
EXPECT_EQ(mutations3[4].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[4].newChildShadowView.tag, 108);
EXPECT_EQ(mutations3[4].newChildShadowView.tag, childI->getTag());
EXPECT_EQ(mutations3[5].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[5].newChildShadowView.tag, 105);
EXPECT_EQ(mutations3[5].newChildShadowView.tag, childF->getTag());
EXPECT_EQ(mutations3[6].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[6].newChildShadowView.tag, 104);
EXPECT_EQ(mutations3[6].newChildShadowView.tag, childE->getTag());
EXPECT_EQ(mutations3[7].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations3[7].newChildShadowView.tag, 103);
EXPECT_EQ(mutations3[7].newChildShadowView.tag, childD->getTag());
EXPECT_EQ(mutations3[8].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[8].newChildShadowView.tag, 105);
EXPECT_EQ(mutations3[8].newChildShadowView.tag, childF->getTag());
EXPECT_EQ(mutations3[9].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[9].newChildShadowView.tag, 104);
EXPECT_EQ(mutations3[9].newChildShadowView.tag, childE->getTag());
EXPECT_EQ(mutations3[10].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[10].newChildShadowView.tag, 103);
EXPECT_EQ(mutations3[10].newChildShadowView.tag, childD->getTag());
EXPECT_EQ(mutations3[11].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[11].newChildShadowView.tag, 107);
EXPECT_EQ(mutations3[11].newChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations3[12].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[12].newChildShadowView.tag, 108);
EXPECT_EQ(mutations3[12].newChildShadowView.tag, childI->getTag());
EXPECT_EQ(mutations3[13].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[13].newChildShadowView.tag, 2000);
EXPECT_EQ(mutations3[13].newChildShadowView.tag, reparentedViewB->getTag());
EXPECT_EQ(mutations3[14].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations3[14].newChildShadowView.tag, 1000);
EXPECT_EQ(mutations3[14].newChildShadowView.tag, reparentedViewA->getTag());
auto mutations4 = calculateShadowViewMutations(*rootNodeV4, *rootNodeV5);
EXPECT_EQ(mutations4.size(), 9);
EXPECT_EQ(mutations4[0].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[0].oldChildShadowView.tag, 106);
EXPECT_EQ(mutations4[0].oldChildShadowView.tag, childG->getTag());
EXPECT_EQ(mutations4[1].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[1].oldChildShadowView.tag, 107);
EXPECT_EQ(mutations4[1].oldChildShadowView.tag, childH->getTag());
EXPECT_EQ(mutations4[2].type, ShadowViewMutation::Update);
EXPECT_EQ(mutations4[2].oldChildShadowView.tag, 108);
EXPECT_EQ(mutations4[2].oldChildShadowView.tag, childI->getTag());
EXPECT_EQ(mutations4[3].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations4[3].oldChildShadowView.tag, 1000);
EXPECT_EQ(mutations4[3].oldChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations4[4].type, ShadowViewMutation::Remove);
EXPECT_EQ(mutations4[4].oldChildShadowView.tag, 2000);
EXPECT_EQ(mutations4[4].oldChildShadowView.tag, reparentedViewB->getTag());
EXPECT_EQ(mutations4[5].type, ShadowViewMutation::Create);
EXPECT_EQ(mutations4[5].newChildShadowView.tag, 109);
EXPECT_EQ(mutations4[5].newChildShadowView.tag, childJ->getTag());
EXPECT_EQ(mutations4[6].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[6].newChildShadowView.tag, 109);
EXPECT_EQ(mutations4[6].newChildShadowView.tag, childJ->getTag());
EXPECT_EQ(mutations4[7].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[7].newChildShadowView.tag, 1000);
EXPECT_EQ(mutations4[7].newChildShadowView.tag, reparentedViewA->getTag());
EXPECT_EQ(mutations4[8].type, ShadowViewMutation::Insert);
EXPECT_EQ(mutations4[8].newChildShadowView.tag, 2000);
EXPECT_EQ(mutations4[8].newChildShadowView.tag, reparentedViewB->getTag());
}
} // namespace facebook::react
@@ -121,8 +121,11 @@ void RCTInstanceSetRuntimeDiagnosticFlags(NSString *flags)
[_bridgeModuleDecorator.callableJSModules
setBridgelessJSModuleMethodInvoker:^(
NSString *moduleName, NSString *methodName, NSArray *args, dispatch_block_t onComplete) {
// TODO: Make RCTInstance call onComplete
[weakSelf callFunctionOnJSModule:moduleName method:methodName args:args];
if (onComplete) {
[weakSelf
callFunctionOnBufferedRuntimeExecutor:[onComplete](facebook::jsi::Runtime &_) { onComplete(); }];
}
}];
}
_launchOptions = launchOptions;
@@ -17,7 +17,7 @@ assertj = "3.21.0"
binary-compatibility-validator = "0.13.2"
download = "5.4.0"
fbjni = "0.7.0"
fresco = "3.4.0"
fresco = "3.5.0"
infer-annotation = "0.18.0"
javax-annotation-api = "1.3.2"
javax-inject = "1"
@@ -366,7 +366,7 @@ class ReactNativePodsUtils
Pod::UI.puts "Setting -DRCT_DYNAMIC_FRAMEWORKS=1 to React-RCTFabric".green
rct_dynamic_framework_flag = " -DRCT_DYNAMIC_FRAMEWORKS=1"
target_installation_result.native_target.build_configurations.each do |config|
prev_build_settings = config.build_settings['OTHER_CPLUSPLUSFLAGS'] != nil ? config.build_settings['OTHER_CPLUSPLUSFLAGS'] : "$(inherithed)"
prev_build_settings = config.build_settings['OTHER_CPLUSPLUSFLAGS'] != nil ? config.build_settings['OTHER_CPLUSPLUSFLAGS'] : "$(inherited)"
config.build_settings['OTHER_CPLUSPLUSFLAGS'] = prev_build_settings + rct_dynamic_framework_flag
end
end
@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {MixedElement} from 'react';
import ReactFabric from '../../../Libraries/Renderer/shims/ReactFabric';
let globalSurfaceIdCounter = 1;
const nativeRuntimeScheduler = global.nativeRuntimeScheduler;
const schedulerPriorityImmediate =
nativeRuntimeScheduler.unstable_ImmediatePriority;
class Root {
#surfaceId: number;
#hasRendered: boolean = false;
constructor() {
this.#surfaceId = globalSurfaceIdCounter;
globalSurfaceIdCounter += 10;
}
render(element: MixedElement) {
if (!this.#hasRendered) {
global.$$JSTesterModuleName$$.startSurface(this.#surfaceId);
this.#hasRendered = true;
}
ReactFabric.render(element, this.#surfaceId, () => {}, true);
}
getMountingLogs(): Array<string> {
return global.$$JSTesterModuleName$$.getMountingManagerLogs(
this.#surfaceId,
);
}
destroy() {
// TODO: check for leaks.
global.$$JSTesterModuleName$$.stopSurface(this.#surfaceId);
global.$$JSTesterModuleName$$.flushMessageQueue();
}
// TODO: add an API to check if all surfaces were deallocated when tests are finished.
}
/*
* Runs a task on on the event loop. To be used together with root.render.
*
* React must run inside of event loop to ensure scheduling environment is closer to production.
*/
export function runTask(task: () => void) {
nativeRuntimeScheduler.unstable_scheduleCallback(
schedulerPriorityImmediate,
task,
);
global.$$JSTesterModuleName$$.flushMessageQueue();
}
// TODO: Add option to define surface props and pass it to startSurface
// Surfacep rops: concurrentRoot, surfaceWidth, surfaceHeight, layoutDirection, pointScaleFactor.
export function createRoot(): Root {
return new Root();
}
@@ -113,7 +113,7 @@ export function createCompositeKeyForProps(
const key = keys[ii];
const value = props[key];
if (allowlist == null || Object.hasOwn(allowlist, key)) {
if (allowlist == null || hasOwn(allowlist, key)) {
let compositeKeyComponent;
if (key === 'style') {
// $FlowFixMe[incompatible-call] - `style` is a valid argument.
@@ -205,7 +205,7 @@ function createCompositeKeyForObject(
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
if (allowlist == null || Object.hasOwn(allowlist, key)) {
if (allowlist == null || hasOwn(allowlist, key)) {
const value = object[key];
let compositeKeyComponent;
@@ -250,7 +250,7 @@ export function areCompositeKeysEqual(
}
for (let ii = 0; ii < length; ii++) {
const key = keys[ii];
if (!Object.hasOwn(next, key)) {
if (!hasOwn(next, key)) {
return false;
}
const prevComponent = prev[key];
@@ -336,7 +336,7 @@ function areCompositeKeyComponentsEqual(
for (let ii = 0; ii < length; ii++) {
const key = keys[ii];
if (
!Object.hasOwn(nullthrows(next), key) ||
!hasOwn(nullthrows(next), key) ||
!areCompositeKeyComponentsEqual(prev[key], next[key])
) {
return false;
@@ -346,3 +346,11 @@ function areCompositeKeyComponentsEqual(
}
return false;
}
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowIgnore[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowIgnore[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
@@ -0,0 +1,169 @@
/**
* 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 {PerformanceObserverCallbackOptions} from '../PerformanceObserver';
import * as ReactNativeTester from '../../../__tests__/ReactNativeTester';
import setUpPerformanceObserver from '../../../setup/setUpPerformanceObserver';
import {PerformanceLongTaskTiming} from '../LongTasks';
import nullthrows from 'nullthrows';
import '../../../../../Libraries/Core/InitializeCore.js';
setUpPerformanceObserver();
function sleep(ms: number) {
const end = performance.now() + ms;
while (performance.now() < end) {}
}
function ensurePerformanceLongTaskTiming(
value: mixed,
): PerformanceLongTaskTiming {
if (!(value instanceof PerformanceLongTaskTiming)) {
throw new Error(
`Expected instance of PerformanceLongTaskTiming but got ${String(value)}`,
);
}
return value;
}
describe('LongTask API', () => {
it('does NOT report short tasks (under 50ms)', () => {
const callback = jest.fn();
const observer = new PerformanceObserver(callback);
observer.observe({entryTypes: ['longtask']});
ReactNativeTester.runTask(() => {
// Short task.
});
expect(callback).not.toHaveBeenCalled();
ReactNativeTester.runTask(() => {
// Slightly longer task, but still not long.
sleep(40);
});
expect(callback).not.toHaveBeenCalled();
});
it('reports long tasks (over 50ms)', () => {
const callback = jest.fn();
const observer = new PerformanceObserver(callback);
observer.observe({entryTypes: ['longtask']});
const beforeTaskStartTime = performance.now();
let afterTaskStartTime;
ReactNativeTester.runTask(() => {
afterTaskStartTime = performance.now();
// Long task.
sleep(51);
});
const afterTaskEndTime = performance.now();
expect(callback).toHaveBeenCalledTimes(1);
const [entries, _observer, options] = callback.mock
.lastCall as $FlowFixMe as [
PerformanceObserverEntryList,
PerformanceObserver,
PerformanceObserverCallbackOptions,
];
expect(_observer).toBe(observer);
expect(options).toEqual({droppedEntriesCount: 0});
const allEntries = entries.getEntries();
expect(allEntries.length).toBe(1);
expect(allEntries[0]).toBeInstanceOf(PerformanceLongTaskTiming);
const entry = ensurePerformanceLongTaskTiming(allEntries[0]);
expect(entry.name).toBe('self');
expect(entry.entryType).toBe('longtask');
expect(entry.startTime).toBeGreaterThanOrEqual(beforeTaskStartTime);
expect(entry.startTime).toBeLessThanOrEqual(nullthrows(afterTaskStartTime));
expect(entry.duration).toBeGreaterThanOrEqual(51);
expect(entry.duration).toBeLessThanOrEqual(
afterTaskEndTime - beforeTaskStartTime,
);
expect(entry.attribution).toEqual([]);
});
describe('tasks that yield', () => {
it('should NOT be reported if they are longer than 50ms but had yielding opportunities in intervals shorter than 50ms', () => {
const callback = jest.fn();
const observer = new PerformanceObserver(callback);
observer.observe({entryTypes: ['longtask']});
const shouldYield = global.nativeRuntimeScheduler.unstable_shouldYield;
ReactNativeTester.runTask(() => {
sleep(40);
shouldYield();
sleep(40);
shouldYield();
sleep(40);
});
expect(callback).not.toHaveBeenCalled();
});
it('should be reported if running for longer than 50ms between yielding opportunities', () => {
const callback = jest.fn();
const observer = new PerformanceObserver(callback);
observer.observe({entryTypes: ['longtask']});
const shouldYield = global.nativeRuntimeScheduler.unstable_shouldYield;
const beforeTaskStartTime = performance.now();
let afterTaskStartTime;
ReactNativeTester.runTask(() => {
afterTaskStartTime = performance.now();
sleep(40);
shouldYield();
sleep(51); // long interval without yielding
shouldYield();
sleep(40);
});
const afterTaskEndTime = performance.now();
expect(callback).toHaveBeenCalledTimes(1);
const entries = callback.mock.lastCall[0] as PerformanceObserverEntryList;
const allEntries = entries.getEntries();
expect(allEntries.length).toBe(1);
const entry = ensurePerformanceLongTaskTiming(allEntries[0]);
expect(entry.name).toBe('self');
expect(entry.entryType).toBe('longtask');
expect(entry.startTime).toBeGreaterThanOrEqual(beforeTaskStartTime);
expect(entry.startTime).toBeLessThanOrEqual(
nullthrows(afterTaskStartTime),
);
expect(entry.duration).toBeGreaterThanOrEqual(131); // just the sum of the sleep times in the task
expect(entry.duration).toBeLessThanOrEqual(
afterTaskEndTime - beforeTaskStartTime,
);
expect(entry.attribution).toEqual([]);
});
});
});
@@ -13,7 +13,7 @@
import type {RNTesterModule} from '../../types/RNTesterTypes';
import * as React from 'react';
import {StyleSheet, TextInput, View, Text} from 'react-native';
import {StyleSheet, Text, TextInput, View} from 'react-native';
const styles = StyleSheet.create({
contents: {
@@ -27,8 +27,8 @@ const {
StyleSheet,
Switch,
Text,
View,
TextInput,
View,
} = require('react-native');
class WithLabel extends React.Component<$FlowFixMeProps> {
@@ -11,6 +11,7 @@
'use strict';
const chalk = require('chalk');
const {execSync: exec} = require('child_process');
const fetch = require('node-fetch');
@@ -30,6 +31,18 @@ type WorkflowRun = {
url: string,
created_at: string,
conclusion: "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required" | null,
head_commit: {
author: {
name: string,
},
message: string,
...
};
triggering_actor: {
login: string,
...
};
run_started_at: string,
};
@@ -102,6 +115,13 @@ async function _getArtifacts(run_id /*: number */) /*: Promise<Artifacts> */ {
return body;
}
function quote(text /*: string*/, prefix /*: string */ = ' > ') {
return text
.split('\n')
.map(line => prefix + line)
.join('\n');
}
// === Public Interface === //
async function initialize(
ciToken /*: string */,
@@ -149,6 +169,21 @@ async function initialize(
) ?? workflow;
}
const commit = workflow.head_commit;
const hours =
(new Date().getTime() - new Date(workflow.run_started_at).getTime()) /
(60 * 60 * 1000);
const started_by = workflow.triggering_actor.login;
console.log(
chalk.green(`The artifact being used is from a workflow started ${chalk.bold.magentaBright(hours.toFixed(0))} hours ago by ${chalk.bold.magentaBright(started_by)}:
Author: ${chalk.bold(commit.author.name)}
Message:
${chalk.magentaBright(quote(commit.message))}
`),
);
artifacts = await _getArtifacts(workflow.id);
}