Compare commits

..
Author SHA1 Message Date
Rob Hogan 9e454af912 Bump Metro to ^0.83.1, lower minimum Node.js version to 20.19
Summary:
Metro release notes: https://github.com/facebook/metro/releases/tag/v0.83.1

The only public-facing change is a lowering of the minimum Node.js version from 22.14 to 20.19.

Changelog: [General][Changed] Metro to ^0.83.1

Differential Revision: D78895160
2025-07-24 07:08:09 -07:00
715 changed files with 6553 additions and 13676 deletions
+5 -1
View File
@@ -75,7 +75,11 @@ module.system.haste.module_ref_prefix=m#
react.runtime=automatic
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FlowFixMeEmpty
ban_spread_key_props=true
@@ -100,4 +104,4 @@ untyped-import
untyped-type-import
[version]
^0.278.0
^0.276.0
@@ -1,6 +1,6 @@
name: 🔍 Debugger - Bug Report
description: Report a bug with React Native DevTools and the New Debugger
labels: ["Needs: Triage :mag:", "Debugging"]
labels: ["Needs: Triage :mag:", "Debugger"]
body:
- type: markdown
@@ -1,83 +0,0 @@
name: Run Fantom Tests
inputs:
release-type:
required: true
description: The type of release we are building. It could be nightly, release or dry-run
gradle-cache-encryption-key:
description: "The encryption key needed to store the Gradle Configuration cache"
runs:
using: composite
steps:
- name: Install dependencies
shell: bash
run: |
sudo apt update
sudo apt install -y git cmake openssl libssl-dev clang
- name: Setup git safe folders
shell: bash
run: git config --global --add safe.directory '*'
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install node dependencies
uses: ./.github/actions/yarn-install
- name: Setup gradle
uses: ./.github/actions/setup-gradle
with:
cache-read-only: "false"
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
- name: Restore Fantom ccache
uses: actions/cache/restore@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactAndroid/**/*.cpp',
'packages/react-native/ReactAndroid/**/*.h',
'packages/react-native/ReactAndroid/**/CMakeLists.txt',
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt',
'private/react-native-fantom/tester/**/*.cpp',
'private/react-native-fantom/tester/**/*.h',
'private/react-native-fantom/tester/**/CMakeLists.txt'
) }}
restore-keys: |
v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-
v2-ccache-fantom-${{ github.job }}-
v2-ccache-fantom-
- name: Show ccache stats
shell: bash
run: ccache -s -v
- name: Run Fantom Tests
shell: bash
run: yarn fantom
env:
CC: clang
CXX: clang++
- name: Save Fantom ccache
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, '-stable') }}
uses: actions/cache/save@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactAndroid/**/*.cpp',
'packages/react-native/ReactAndroid/**/*.h',
'packages/react-native/ReactAndroid/**/CMakeLists.txt',
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt',
'private/react-native-fantom/tester/**/*.cpp',
'private/react-native-fantom/tester/**/*.h',
'private/react-native-fantom/tester/**/CMakeLists.txt'
) }}
- name: Show ccache stats
shell: bash
run: ccache -s -v
- name: Upload test results
if: ${{ always() }}
uses: actions/upload-artifact@v4.3.4
with:
name: run-fantom-tests-results
compression-level: 1
path: |
private/react-native-fantom/build/reports
@@ -1,897 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
'use strict';
const {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
} = require('../firebaseUtils');
describe('FirebaseClient', () => {
const originalFetch = global.fetch;
const originalEnv = process.env;
beforeEach(() => {
global.fetch = jest.fn();
process.env = {
...originalEnv,
FIREBASE_APP_EMAIL: 'test@example.com',
FIREBASE_APP_PASS: 'testpassword',
FIREBASE_APP_APIKEY: 'test-api-key',
FIREBASE_APP_PROJECTNAME: 'test-project',
};
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
global.fetch = originalFetch;
process.env = originalEnv;
jest.restoreAllMocks();
});
describe('constructor', () => {
it('should initialize with environment variables', () => {
const client = new FirebaseClient();
expect(client.email).toBe('test@example.com');
expect(client.password).toBe('testpassword');
expect(client.apiKey).toBe('test-api-key');
expect(client.projectId).toBe('test-project');
expect(client.databaseUrl).toBe(
'test-project-default-rtdb.firebaseio.com',
);
expect(client.idToken).toBeNull();
});
});
describe('authenticate', () => {
it('should authenticate successfully', async () => {
const mockResponse = {
idToken: 'mock-id-token',
refreshToken: 'mock-refresh-token',
};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResponse)),
});
const client = new FirebaseClient();
await client.authenticate();
expect(client.idToken).toBe('mock-id-token');
expect(global.fetch).toHaveBeenCalledWith(
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=test-api-key',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'test@example.com',
password: 'testpassword',
returnSecureToken: true,
}),
},
);
});
it('should throw error when email is missing', async () => {
delete process.env.FIREBASE_APP_EMAIL;
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'Firebase credentials not found in environment variables',
);
});
it('should throw error when password is missing', async () => {
delete process.env.FIREBASE_APP_PASS;
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'Firebase credentials not found in environment variables',
);
});
it('should handle authentication failure', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: jest.fn().mockResolvedValueOnce(
JSON.stringify({
error: {message: 'Invalid credentials'},
}),
),
});
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'HTTP 400: Invalid credentials',
);
});
});
describe('makeRequest', () => {
it('should make successful GET request', async () => {
const mockData = {test: 'data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
const result = await client.makeRequest('example.com', '/test', 'GET');
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
});
it('should make successful POST request with data', async () => {
const mockData = {success: true};
const postData = {test: 'post data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
const result = await client.makeRequest(
'example.com',
'/test',
'POST',
postData,
);
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
});
it('should handle non-JSON response', async () => {
const textResponse = 'plain text response';
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(textResponse),
});
const client = new FirebaseClient();
const result = await client.makeRequest('example.com', '/test', 'GET');
expect(result).toBe(textResponse);
});
it('should handle HTTP error with JSON error message', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce(
JSON.stringify({
error: {message: 'Not found'},
}),
),
});
const client = new FirebaseClient();
await expect(
client.makeRequest('example.com', '/test', 'GET'),
).rejects.toThrow('HTTP 404: Not found');
});
it('should handle HTTP error with plain text error message', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
});
const client = new FirebaseClient();
await expect(
client.makeRequest('example.com', '/test', 'GET'),
).rejects.toThrow('HTTP 500: Internal Server Error');
});
});
describe('makeDatabaseRequest', () => {
it('should make database request with existing token', async () => {
const mockData = {test: 'data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.makeDatabaseRequest('2023-12-01', 'GET');
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
});
it('should authenticate before making request if no token exists', async () => {
const authResponse = {idToken: 'new-token'};
const dataResponse = {test: 'data'};
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(dataResponse)),
});
const client = new FirebaseClient();
const result = await client.makeDatabaseRequest('2023-12-01', 'GET');
expect(result).toEqual(dataResponse);
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(client.idToken).toBe('new-token');
});
it('should make PUT request with data', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const testData = [{library: 'test', status: 'success'}];
await client.makeDatabaseRequest('2023-12-01', 'PUT', testData);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testData),
},
);
});
});
describe('storeResults', () => {
it('should store results successfully', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = [{library: 'test', status: 'success'}];
await client.storeResults('2023-12-01', results);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(results),
},
);
expect(console.log).toHaveBeenCalledWith(
'Successfully stored results for 2023-12-01',
);
});
});
describe('getResults', () => {
it('should retrieve results successfully', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = await client.getResults('2023-12-01');
expect(results).toEqual(mockResults);
});
it('should return null for 404 errors', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = await client.getResults('2023-12-01');
expect(results).toBeNull();
});
it('should throw error for non-404 HTTP errors', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
await expect(client.getResults('2023-12-01')).rejects.toThrow(
'HTTP 500: Internal Server Error',
);
});
});
describe('getLatestResults', () => {
it('should authenticate before making requests if no token exists', async () => {
const authResponse = {idToken: 'new-token'};
const mockResults = [{library: 'test', status: 'success'}];
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
const result = await client.getLatestResults('2023-12-15', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-12-14',
});
expect(client.idToken).toBe('new-token');
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-14 (1 days back)',
);
});
it('should find results from the previous day', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-14',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-14 (1 days back)',
);
});
it('should find results from several days back', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock 404 responses for first 2 days, then success on 3rd day
global.fetch
.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
})
.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-12',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-13 (2 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-12 (3 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-12 (3 days back)',
);
});
it('should skip empty results and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock empty array for first day, then valid results on second day
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify([])),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-13 (2 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-13 (2 days back)',
);
});
it('should return null when no results found within maxDaysBack', async () => {
// Mock 404 responses for all days
global.fetch.mockResolvedValue({
ok: false,
status: 404,
text: jest.fn().mockResolvedValue('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 3);
expect(result).toEqual({
results: null,
date: null,
});
expect(console.log).toHaveBeenCalledWith(
'No previous results found within the last 3 days',
);
expect(global.fetch).toHaveBeenCalledTimes(3);
});
it('should use default maxDaysBack of 7 when not specified', async () => {
// Mock 404 responses for all days
global.fetch.mockResolvedValue({
ok: false,
status: 404,
text: jest.fn().mockResolvedValue('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15');
expect(result).toEqual({
results: null,
date: null,
});
expect(console.log).toHaveBeenCalledWith(
'No previous results found within the last 7 days',
);
expect(global.fetch).toHaveBeenCalledTimes(7);
});
it('should handle non-404 errors and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock 500 error for first day, then success on second day
global.fetch
.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
expect(console.log).toHaveBeenCalledWith(
'No results found for 2023-12-14: HTTP 500: Internal Server Error',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-13 (2 days back)',
);
});
it('should handle date boundaries correctly', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
// Test month boundary
const result = await client.getLatestResults('2023-12-01', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-11-30',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-11-30 (1 days back)...',
);
});
it('should handle year boundary correctly', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
// Test year boundary
const result = await client.getLatestResults('2024-01-01', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-12-31',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-31 (1 days back)...',
);
});
it('should handle null results and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock null for first day, then valid results on second day
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
});
});
});
describe('compareResults', () => {
it('should handle null previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, null);
expect(result).toEqual({
broken: [],
recovered: [],
newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}],
});
});
it('should handle undefined previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
];
const result = compareResults(currentResults, undefined);
expect(result).toEqual({
broken: [],
recovered: [],
newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}],
});
});
it('should identify broken tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([]);
});
it('should identify recovered tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'failed',
currentStatus: 'success',
},
]);
});
it('should identify both broken and recovered tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
{library: 'lib3', platform: 'iOS', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'failed'},
{library: 'lib3', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([
{
library: 'lib2',
platform: 'Android',
previousStatus: 'failed',
currentStatus: 'success',
},
]);
});
it('should handle tests that are not in previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([]);
});
it('should handle empty current results', () => {
const currentResults = [];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([]);
});
it('should handle empty previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
];
const previousResults = [];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([]);
// When previousResults is an empty array (not null/undefined),
// the function doesn't return newFailures property
expect(result.newFailures).toBeUndefined();
});
it('should handle different status values', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'timeout'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'error'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'timeout',
},
]);
expect(result.recovered).toEqual([
{
library: 'lib2',
platform: 'Android',
previousStatus: 'error',
currentStatus: 'success',
},
]);
});
});
describe('getYesterdayDate', () => {
it("should return yesterday's date in YYYY-MM-DD format", () => {
const mockDate = new Date('2023-12-15T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-12-14');
global.Date.mockRestore();
});
it('should handle month boundary correctly', () => {
const mockDate = new Date('2023-12-01T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-11-30');
global.Date.mockRestore();
});
it('should handle year boundary correctly', () => {
const mockDate = new Date('2024-01-01T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-12-31');
global.Date.mockRestore();
});
});
describe('getTodayDate', () => {
it("should return today's date in YYYY-MM-DD format", () => {
const mockDate = new Date('2023-12-15T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getTodayDate();
expect(result).toBe('2023-12-15');
global.Date.mockRestore();
});
it('should handle different times of day correctly', () => {
const mockDate = new Date('2023-12-15T23:59:59Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getTodayDate();
expect(result).toBe('2023-12-15');
global.Date.mockRestore();
});
});
@@ -117,13 +117,13 @@ describe('#verifyPublishedTemplate', () => {
it('will timeout if npm does not update package version after a set number of retries', async () => {
const RETRIES = 2;
(await verifyPublishedTemplate('0.77.0', true, RETRIES),
await verifyPublishedTemplate('0.77.0', true, RETRIES),
expect(mockVerifyPublishedPackage).toHaveBeenCalledWith(
'@react-native-community/template',
'0.77.0',
'latest',
2,
));
);
});
});
});
@@ -83,13 +83,13 @@ describe('#verifyReleaseOnNPM', () => {
it('will timeout if npm does not update package version after a set number of retries', async () => {
const RETRIES = 2;
(await verifyReleaseOnNpm('0.77.0', true, RETRIES),
await verifyReleaseOnNpm('0.77.0', true, RETRIES),
expect(mockVerifyPublishedPackage).toHaveBeenCalledWith(
'react-native',
'0.77.0',
'latest',
2,
));
);
});
it('will timeout if npm does not update latest tag after a set number of retries', async () => {
@@ -11,15 +11,8 @@ const fs = require('fs');
const path = require('path');
const {
prepareFailurePayload,
prepareComparisonPayload,
sendMessageToDiscord,
} = require('./notifyDiscord');
const {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
} = require('./firebaseUtils');
function readOutcomes() {
const baseDir = '/tmp';
@@ -110,62 +103,16 @@ async function collectResults(discordWebHook) {
const outcomes = readOutcomes();
const failures = printFailures(outcomes);
// Send failure notification if there are current failures
if (failures.length > 0) {
if (discordWebHook) {
console.log('Sending current failures to Discord...');
console.log('Sending to discord');
await notifyDiscord(discordWebHook, failures);
} else {
console.log('Discord webhook not set');
console.log('Web hook not set');
}
process.exit(1);
}
// Initialize Firebase client
const firebaseClient = new FirebaseClient();
const today = getTodayDate();
try {
// Store today's results in Firebase
console.log(`Storing results for ${today} in Firebase...`);
await firebaseClient.storeResults(today, outcomes);
// Get the most recent previous results for comparison
console.log(`Looking for most recent previous results before ${today}...`);
const {results: previousResults, date: previousDate} =
await firebaseClient.getLatestResults(today);
let broken = [];
let recovered = [];
if (previousResults) {
console.log(`Comparing with results from ${previousDate}`);
// Compare results and identify broken/recovered jobs
const comparison = compareResults(outcomes, previousResults);
broken = comparison.broken;
recovered = comparison.recovered;
console.log(
`Found ${broken.length} newly broken jobs and ${recovered.length} recovered jobs compared to ${previousDate}`,
);
} else {
console.log(
'No previous results found for comparison - this might be the first run or no recent data available',
);
}
// Send comparison message to Discord if there are changes
if (discordWebHook && (broken.length > 0 || recovered.length > 0)) {
console.log('Sending comparison results to Discord...');
const comparisonMessage = prepareComparisonPayload(broken, recovered);
await sendMessageToDiscord(discordWebHook, comparisonMessage);
}
console.log('✅ All tests passed!');
} catch (error) {
console.error('Error in collectResults:', error);
// If Firebase fails but there are no test failures, don't fail the workflow
console.log('⚠️ Firebase operations failed, but all tests passed');
}
console.log('✅ All tests passed!');
}
module.exports = {
@@ -25,31 +25,6 @@ function extractUsersFromScheduleAndDate(schedule, userMap, date) {
return [user1, user2];
}
/**
* You can invoke this script by doing:
* ```
* node .github/workflow-scripts/extractIssueOncalls.js $DATA
* ```
*
* the $DATA is stored in the github secrets as ONCALL_SCHEDULE variable.
* The format of the data is:
* ```
* {
* \"userMap\": {
* \"discord_handle1\": \"discord_id1\",
* \"discord_handle2\": \"discord_id2\",
* ...
* },
* \"schedule\": {
* \"2025-07-29\": [\"discord_handle1\", \"discord_handle2\"],
* \"2025-08-05\": [\"discord_handle3\", \"discord_handle4\"],
* ...
* }
* ```
*
* When uploading the secret, make sure that the JSON strings are escaped!
* The script will fail otherwise, because GitHub will remove the `"` characters.
*/
function main() {
const configuration = process.argv[2];
const {userMap, schedule} = JSON.parse(configuration);
-261
View File
@@ -1,261 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
// We connect to firebase using a plain HTTP request because we don't want to
// add yet another devDependency to the react-native monorepo.
class FirebaseClient {
constructor() {
this.email = process.env.FIREBASE_APP_EMAIL;
this.password = process.env.FIREBASE_APP_PASS;
this.apiKey = process.env.FIREBASE_APP_APIKEY;
this.projectId = process.env.FIREBASE_APP_PROJECTNAME;
this.databaseUrl = `${this.projectId}-default-rtdb.firebaseio.com`;
this.idToken = null;
}
async authenticate() {
if (!this.email || !this.password) {
throw new Error(
'Firebase credentials not found in environment variables',
);
}
const authData = {
email: this.email,
password: this.password,
returnSecureToken: true,
};
const response = await this.makeRequest(
'identitytoolkit.googleapis.com',
`/v1/accounts:signInWithPassword?key=${this.apiKey}`,
'POST',
authData,
);
this.idToken = response.idToken;
return;
}
/**
* Make a database request for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {string} method - HTTP method
* @param {*} data - Data to send (optional)
* @returns {Promise<*>} - Response data
*/
async makeDatabaseRequest(date, method, data = null) {
if (!this.idToken) {
await this.authenticate();
}
const path = `/nightly-results/${date}.json?auth=${this.idToken}`;
return this.makeRequest(this.databaseUrl, path, method, data);
}
/**
* Store test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {Array<Object>} results - Array of test results
* @returns {Promise<void>}
*/
async storeResults(date, results) {
await this.makeDatabaseRequest(date, 'PUT', results);
console.log(`Successfully stored results for ${date}`);
}
/**
* Retrieve test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @returns {Promise<Array<Object>|null>} - Array of test results or null if not found
*/
async getResults(date) {
try {
return await this.makeDatabaseRequest(date, 'GET');
} catch (error) {
if (error.message.includes('404')) {
return null; // No results found for this specific date.
}
throw error;
}
}
/**
* Find the most recent available job results before the given date
* @param {string} currentDate - Current date in YYYY-MM-DD format
* @param {number} maxDaysBack - Maximum number of days to look back (default: 7)
* @returns {Promise<{results: Array<Object>|null, date: string|null}>} - Most recent results and their date
*/
async getLatestResults(currentDate, maxDaysBack = 7) {
if (!this.idToken) {
await this.authenticate();
}
const currentDateObj = new Date(currentDate);
for (let daysBack = 1; daysBack <= maxDaysBack; daysBack++) {
const checkDate = new Date(currentDateObj);
checkDate.setDate(checkDate.getDate() - daysBack);
const checkDateStr = checkDate.toISOString().split('T')[0];
console.log(
`Checking for results on ${checkDateStr} (${daysBack} days back)...`,
);
try {
const results = await this.getResults(checkDateStr);
if (results && results.length > 0) {
console.log(
`Found results from ${checkDateStr} (${daysBack} days back)`,
);
return {results, date: checkDateStr};
}
} catch (error) {
console.log(`No results found for ${checkDateStr}: ${error.message}`);
continue;
}
}
console.log(
`No previous results found within the last ${maxDaysBack} days`,
);
return {results: null, date: null};
}
async makeRequest(hostname, path, method, data = null) {
const url = `https://${hostname}${path}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
},
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
const responseText = await response.text();
if (!response.ok) {
let errorMessage;
try {
const parsedError = JSON.parse(responseText);
errorMessage = parsedError.error?.message || responseText;
} catch {
errorMessage = responseText;
}
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
}
try {
return JSON.parse(responseText);
} catch {
return responseText;
}
}
}
/**
* Compare current results with previous day's results
* @param {Array<Object>} currentResults - Today's test results
* @param {Array<Object>} previousResults - Yesterday's test results
* @returns {Object} - Object containing broken and recovered tests
*/
function compareResults(currentResults, previousResults) {
if (!previousResults) {
return {
broken: [],
recovered: [],
newFailures: currentResults.filter(result => result.status !== 'success'),
};
}
// Create maps for easier lookup
const currentMap = new Map();
const previousMap = new Map();
currentResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
currentMap.set(key, result);
});
previousResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
previousMap.set(key, result);
});
const broken = [];
const recovered = [];
// Check for broken tests (was success, now failed)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status === 'success' &&
currentResult.status !== 'success'
) {
broken.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
// Check for recovered tests (was failed, now success)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status !== 'success' &&
currentResult.status === 'success'
) {
recovered.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
return {broken, recovered};
}
/**
* Get yesterday's date in YYYY-MM-DD format
* @returns {string} - Yesterday's date
*/
function getYesterdayDate() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return yesterday.toISOString().split('T')[0];
}
/**
* Get today's date in YYYY-MM-DD format
* @returns {string} - Today's date
*/
function getTodayDate() {
const today = new Date();
return today.toISOString().split('T')[0];
}
module.exports = {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
};
+14 -60
View File
@@ -40,28 +40,6 @@ async function sendMessageToDiscord(webHook, message) {
}
}
/**
* Sorts jobs by platform first, then by library name.
* @param {Array<Object>} jobs - Array of jobs with platform and library properties
* @returns {Array<Object>} - Sorted array of jobs
*/
function sortResultsByPlatformAndLibrary(jobs) {
return [...jobs].sort((a, b) => {
// First sort by platform
const platformA = a.platform || 'Unknown';
const platformB = b.platform || 'Unknown';
if (platformA !== platformB) {
return platformA.localeCompare(platformB);
}
// Then sort by library name
const libraryA = a.library || 'Unknown';
const libraryB = b.library || 'Unknown';
return libraryA.localeCompare(libraryB);
});
}
/**
* Prepares a formatted Discord message payload from a list of failures.
* @param {Array<Object>} failures - List of failures to format
@@ -76,7 +54,20 @@ function prepareFailurePayload(failures) {
}
// Sort failures by platform and then by library name
const sortedFailures = sortResultsByPlatformAndLibrary(failures);
const sortedFailures = [...failures].sort((a, b) => {
// First sort by platform
const platformA = a.platform || 'Unknown';
const platformB = b.platform || 'Unknown';
if (platformA !== platformB) {
return platformA.localeCompare(platformB);
}
// Then sort by library name
const libraryA = a.library || 'Unknown';
const libraryB = b.library || 'Unknown';
return libraryA.localeCompare(libraryB);
});
// Format the failures into a message
const formattedFailures = sortedFailures
@@ -92,45 +83,8 @@ function prepareFailurePayload(failures) {
};
}
/**
* Prepares a formatted Discord message payload for broken and recovered nightly jobs.
* @param {Array<Object>} broken - List of newly broken jobs
* @param {Array<Object>} recovered - List of recovered jobs
* @returns {Object} - The formatted Discord message payload
*/
function prepareComparisonPayload(broken, recovered) {
let content = '📊 **React Native Nightly Integration Status Update** 📊\n\n';
if (broken.length === 0 && recovered.length === 0) {
content +=
'No changes from yesterday - all nightly jobs maintained their previous status.';
} else {
if (broken.length > 0) {
content += '🔴 **Newly Broken Jobs:**\n';
const sortedBroken = sortResultsByPlatformAndLibrary(broken);
sortedBroken.forEach(job => {
content += `❌ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`;
});
content += '\n';
}
if (recovered.length > 0) {
content += '🟢 **Recovered Jobs:**\n';
const sortedRecovered = sortResultsByPlatformAndLibrary(recovered);
sortedRecovered.forEach(job => {
content += `✅ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`;
});
}
}
return {content};
}
// Export the functions using CommonJS syntax
module.exports = {
prepareFailurePayload,
prepareComparisonPayload,
sendMessageToDiscord,
};
-4
View File
@@ -32,7 +32,3 @@ jobs:
needs: check-nightly
secrets:
discord_webhook_url: ${{ secrets.NIGHTLY_DISCORD_WEBHOOK }}
firebase_app_email: ${{ secrets.FIREBASE_APP_EMAIL }}
firebase_app_pass: ${{ secrets.FIREBASE_APP_PASS }}
firebase_app_projectname: ${{ secrets.FIREBASE_APP_PROJECTNAME }}
firebase_app_apikey: ${{ secrets.FIREBASE_APP_APIKEY }}
+4
View File
@@ -27,6 +27,10 @@ jobs:
ONCALL2=$(echo $ONCALLS | cut -d ' ' -f 2)
echo "oncall1=$ONCALL1" >> $GITHUB_ENV
echo "oncall2=$ONCALL2" >> $GITHUB_ENV
- name: Print oncalls
run: |
echo "oncall1: ${{ env.oncall1 }}"
echo "oncall2: ${{ env.oncall2 }}"
- name: Monitor New Issues
uses: react-native-community/repo-monitor@v1.0.1
with:
+2 -20
View File
@@ -370,25 +370,6 @@ jobs:
hermes-version: ${{ needs.prepare_hermes_workspace.outputs.hermes-version }}
react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }}
run_fantom_tests:
runs-on: 8-core-ubuntu
needs: [set_release_type]
container:
image: reactnativecommunity/react-native-android:latest
env:
TERM: "dumb"
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build and Test Fantom
uses: ./.github/actions/run-fantom-tests
with:
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
build_hermesc_windows:
runs-on: windows-2025
needs: prepare_hermes_workspace
@@ -592,7 +573,8 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: ["24.4.1", "22", "20.19.4"]
node-version: ["24", "22"]
# node-version: ["24", "22", "20.19.4"]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -5,14 +5,6 @@ on:
secrets:
discord_webhook_url:
required: true
firebase_app_email:
required: true
firebase_app_pass:
required: true
firebase_app_apikey:
required: true
firebase_app_projectname:
required: true
# We use the matrix.library entry to specify the dependency we want to use
@@ -102,11 +94,6 @@ jobs:
path: /tmp
- name: Collect failures
uses: actions/github-script@v6
env:
FIREBASE_APP_EMAIL: ${{ secrets.firebase_app_email }}
FIREBASE_APP_PASS: ${{ secrets.firebase_app_pass }}
FIREBASE_APP_APIKEY: ${{ secrets.firebase_app_apikey }}
FIREBASE_APP_PROJECTNAME: ${{ secrets.firebase_app_projectname }}
with:
script: |
const {collectResults} = require('./.github/workflow-scripts/collectNightlyOutcomes.js');
-1
View File
@@ -170,7 +170,6 @@ fix_*.patch
# Jest Integration
/private/react-native-fantom/build/
/private/react-native-fantom/.out/
/private/react-native-fantom/tester/build/
# [Experimental] Generated TS type definitions
+393 -256
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -63,7 +63,7 @@
"@typescript-eslint/parser": "^8.36.0",
"ansi-styles": "^4.2.1",
"babel-plugin-minify-dead-code-elimination": "^0.5.2",
"babel-plugin-syntax-hermes-parser": "0.30.0",
"babel-plugin-syntax-hermes-parser": "0.29.1",
"babel-plugin-transform-define": "^2.1.4",
"babel-plugin-transform-flow-enums": "^0.0.2",
"clang-format": "^1.8.0",
@@ -81,11 +81,11 @@
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-redundant-undefined": "^0.4.0",
"eslint-plugin-relay": "^1.8.3",
"flow-api-translator": "0.30.0",
"flow-bin": "^0.278.0",
"flow-api-translator": "0.29.1",
"flow-bin": "^0.276.0",
"glob": "^7.1.1",
"hermes-eslint": "0.30.0",
"hermes-transform": "0.30.0",
"hermes-eslint": "0.29.1",
"hermes-transform": "0.29.1",
"ini": "^5.0.0",
"inquirer": "^7.1.0",
"jest": "^29.7.0",
@@ -101,10 +101,10 @@
"micromatch": "^4.0.4",
"node-fetch": "^2.2.0",
"nullthrows": "^1.1.1",
"prettier": "3.6.2",
"prettier-plugin-hermes-parser": "0.31.1",
"react": "19.1.1",
"react-test-renderer": "19.1.1",
"prettier": "2.8.8",
"prettier-plugin-hermes-parser": "0.29.1",
"react": "19.1.0",
"react-test-renderer": "19.1.0",
"rimraf": "^3.0.2",
"shelljs": "^0.8.5",
"signedsource": "^1.0.0",
@@ -116,8 +116,6 @@
},
"resolutions": {
"eslint-plugin-react-hooks": "6.1.0-canary-12bc60f5-20250613",
"react-is": "19.1.1",
"on-headers": "1.1.0",
"compression": "1.8.1"
"react-is": "19.1.0"
}
}
+3 -3
View File
@@ -24,12 +24,12 @@ try {
} catch (e) {
// Fallback to lib when source doesn't exit (e.g. when installed as a dev dependency)
FlowParser =
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
require('@react-native/codegen/lib/parsers/flow/parser').FlowParser;
TypeScriptParser =
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
require('@react-native/codegen/lib/parsers/typescript/parser').TypeScriptParser;
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen');
}
@@ -80,7 +80,7 @@ try {
'@react-native-community/cli-server-api',
{paths: [communityCliPath]},
);
// $FlowFixMe[unsupported-syntax] dynamic import
// $FlowIgnore[unsupported-syntax] dynamic import
communityMiddlewareFallback.createDevServerMiddleware = require(
communityCliServerApiPath,
).createDevServerMiddleware as CreateDevServerMiddleware;
@@ -92,14 +92,14 @@ async function runServer(
console.info(`Starting dev server on ${devServerUrl}\n`);
if (args.assetPlugins) {
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.transformer.assetPlugins = args.assetPlugins.map(plugin =>
require.resolve(plugin),
);
}
// TODO(T214991636): Remove legacy Metro log forwarding
if (!args.clientLogs) {
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.server.forwardClientLogs = false;
}
@@ -127,8 +127,6 @@ async function runServer(
const reporter: Reporter = {
update(event: TerminalReportableEvent) {
terminalReporter.update(event);
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (reportEvent) {
reportEvent(event);
}
@@ -146,7 +144,7 @@ async function runServer(
}
},
};
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.reporter = reporter;
await Metro.runServer(metroConfig, {
@@ -175,7 +173,7 @@ function getReporterImpl(
try {
// First we let require resolve it, so we can require packages in node_modules
// as expected. eg: require('my-package/reporter');
// $FlowFixMe[unsupported-syntax]
// $FlowIgnore[unsupported-syntax]
return require(customLogReporterPath);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
@@ -183,7 +181,7 @@ function getReporterImpl(
}
// If that doesn't work, then we next try relative to the cwd, eg:
// require('./reporter');
// $FlowFixMe[unsupported-syntax]
// $FlowIgnore[unsupported-syntax]
return require(path.resolve(customLogReporterPath));
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ const FIRST = 1,
FOURTH = 4;
function getNodePackagePath(packageName: string): string {
// $FlowFixMe[prop-missing] type definition is incomplete
// $FlowIgnore[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
+1 -1
View File
@@ -75,7 +75,7 @@ const FIRST = 1,
FIFTH = 5;
function getNodePackagePath(packageName: string): string {
// $FlowFixMe[prop-missing] type definition is incomplete
// $FlowIgnore[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
+2 -2
View File
@@ -1,5 +1,5 @@
@generated SignedSource<<9252db36d4b1db907a38c08935ceeb38>>
Git revision: 921566790e9e16d0ecace6e49b3cfaace205958c
@generated SignedSource<<74c5fb174ae5a8a3850a3c2373b3b6f5>>
Git revision: a7e4f59675edbda995b0cb0d40f277a59a3baebf
Built with --nohooks: false
Is local checkout: false
Remote URL: https://github.com/facebook/react-native-devtools-frontend
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,13 +19,13 @@ const semver = require('semver');
// safety to ensure the target of the resolution is in sync with the declared dependency.
describe('Electron dependency', () => {
test('should be semver-satisfied by the actual electron version', () => {
// $FlowFixMe[untyped-import] - package.json is not typed
// $FlowIssue[untyped-import] - package.json is not typed
const ourPackageJson = require('../package.json');
const declaredElectronVersion = ourPackageJson.dependencies.electron;
expect(declaredElectronVersion).toBeTruthy();
// $FlowFixMe[untyped-import] - package.json is not typed
// $FlowIssue[untyped-import] - package.json is not typed
const electronPackageJson = require('electron/package.json');
const actualElectronVersion = electronPackageJson.version;
@@ -16,7 +16,7 @@ contextBridge.executeInMainWorld({
let didDecorateInspectorFrontendHostInstance = false;
// reactNativeDecorateInspectorFrontendHostInstance was introduced in
// https://github.com/facebook/react-native-devtools-frontend/pull/168
// $FlowFixMe[prop-missing]
// $FlowIgnore[prop-missing]
globalThis.reactNativeDecorateInspectorFrontendHostInstance = (
InspectorFrontendHostInstance: $FlowFixMe,
) => {
+2 -2
View File
@@ -18,9 +18,9 @@ declare module.exports: typeof Node;
// Because Electron doesn't support package.json `exports`, we need to
// switch at runtime.
if ('electron' in process.versions) {
// $FlowFixMe[invalid-export]
// $FlowIgnore[invalid-export]
module.exports = require('./electron');
} else {
// $FlowFixMe[invalid-export]
// $FlowIgnore[invalid-export]
module.exports = require('./node');
}
+1 -1
View File
@@ -39,7 +39,7 @@
},
"devDependencies": {
"selfsigned": "^2.4.1",
"undici": "^5.29.0",
"undici": "^5.28.5",
"wait-for-expect": "^3.0.2"
}
}
@@ -70,7 +70,7 @@ export class DebuggerAgent {
this.#ws = null;
}
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get socket(): WebSocket {
return nullthrows(this.#ws);
}
@@ -103,11 +103,11 @@ export class DebuggerMock extends DebuggerAgent {
originalHandleCallsArray === this.handle.mock.calls
? this.handle.mock.calls.slice(originalHandleCallCount)
: this.handle.mock.calls;
// $FlowFixMe[incompatible-use]
// $FlowFixMe[prop-missing]
// $FlowIgnore[incompatible-use]
// $FlowIgnore[prop-missing]
const [response] = newHandleCalls.find(args => args[0].id === message.id);
// $FlowFixMe[incompatible-return]
// $FlowFixMe[incompatible-indexer]
// $FlowIgnore[incompatible-return]
// $FlowIgnore[incompatible-indexer]
return response;
}
}
@@ -90,7 +90,7 @@ export class DeviceAgent {
});
}
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get socket(): WebSocket {
return nullthrows(this.#ws);
}
@@ -64,9 +64,9 @@ export async function sendFromTargetToDebugger<Message: CdpMessageFromTarget>(
originalHandleCallsArray === debugger_.handle.mock.calls
? debugger_.handle.mock.calls.slice(originalHandleCallCount)
: debugger_.handle.mock.calls;
// $FlowFixMe[incompatible-type]
// $FlowIgnore[incompatible-type]
const [receivedMessage]: [Message] = newHandleCalls.find(
// $FlowFixMe[incompatible-call]
// $FlowIgnore[incompatible-call]
(call: [Message]) => call[0].method === message.method,
);
return receivedMessage;
@@ -97,13 +97,13 @@ export async function sendFromDebuggerToTarget<Message: CdpMessageToTarget>(
originalEventCallsArray === device.wrappedEventParsed.mock.calls
? device.wrappedEventParsed.mock.calls.slice(originalEventCallCount)
: device.wrappedEventParsed.mock.calls;
// $FlowFixMe[incompatible-use]
// $FlowIgnore[incompatible-use]
const [receivedMessage] = newEventCalls.find(
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-use]
// $FlowIgnore[prop-missing]
// $FlowIgnore[incompatible-use]
call => call[0].wrappedEvent.id === message.id,
);
// $FlowFixMe[incompatible-return]
// $FlowIgnore[incompatible-return]
return receivedMessage.wrappedEvent;
}
@@ -141,7 +141,7 @@ export async function createAndConnectTarget(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -60,7 +60,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -119,7 +119,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -187,7 +187,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -288,7 +288,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -338,7 +338,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -108,7 +108,7 @@ describe('inspector proxy device message middleware', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -314,7 +314,7 @@ describe('inspector-proxy device socket handoff', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toEqual(
expect.arrayContaining(
@@ -55,7 +55,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -113,7 +113,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -169,7 +169,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -222,7 +222,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -273,7 +273,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -423,7 +423,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -459,7 +459,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -10,7 +10,7 @@
export function withAbortSignalForEachTest(): $ReadOnly<{signal: AbortSignal}> {
const ref: {signal: AbortSignal} = {
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get signal() {
throw new Error(
'The return value of withAbortSignalForEachTest is lazily initialized and can only be accessed in tests.',
@@ -39,19 +39,19 @@ export function withServerForEachTest(options: CreateServerOptions): $ReadOnly<{
app: ConnectApp,
port: number,
} = {
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get serverBaseUrl() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get serverBaseWsUrl() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get app() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get port() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
@@ -62,11 +62,11 @@ export function withServerForEachTest(options: CreateServerOptions): $ReadOnly<{
({server, app} = await createServer(options));
const serverBaseUrl = baseUrlForServer(
server,
(options.secure ?? false) ? 'https' : 'http',
options.secure ?? false ? 'https' : 'http',
);
const serverBaseWsUrl = baseUrlForServer(
server,
(options.secure ?? false) ? 'wss' : 'ws',
options.secure ?? false ? 'wss' : 'ws',
);
Object.defineProperty(ref, 'serverBaseUrl', {value: serverBaseUrl});
Object.defineProperty(ref, 'serverBaseWsUrl', {value: serverBaseWsUrl});
@@ -40,7 +40,7 @@ function makeRequest(
host: ?string,
encrypted: boolean,
): http$IncomingMessage<> | http$IncomingMessage<tls$TLSSocket> {
// $FlowFixMe[incompatible-return] Partial mock of request
// $FlowIgnore[incompatible-return] Partial mock of request
return {
socket: encrypted ? {encrypted: true} : {},
headers: host != null ? {host} : {},
@@ -39,6 +39,6 @@
},
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "3.6.2"
"prettier": "2.8.8"
}
}
@@ -18,8 +18,8 @@
"bugs": "https://github.com/facebook/react-native/issues",
"main": "index.js",
"devDependencies": {
"babel-plugin-syntax-hermes-parser": "0.30.0",
"hermes-eslint": "0.30.0"
"babel-plugin-syntax-hermes-parser": "0.29.1",
"hermes-eslint": "0.29.1"
},
"engines": {
"node": ">= 20.19.4"
+2 -2
View File
@@ -32,8 +32,8 @@
"source-map-support": "0.5.0"
},
"devDependencies": {
"babel-plugin-syntax-hermes-parser": "0.30.0",
"hermes-eslint": "0.30.0"
"babel-plugin-syntax-hermes-parser": "0.29.1",
"hermes-eslint": "0.29.1"
},
"engines": {
"node": ">= 20.19.4"
@@ -1,5 +1,5 @@
[versions]
agp = "8.12.0"
agp = "8.11.0"
gson = "2.8.9"
guava = "31.0.1-jre"
javapoet = "1.13.0"
@@ -28,8 +28,8 @@ import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
import com.facebook.react.utils.ProjectUtils.isNewArchEnabled
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import com.facebook.react.utils.PropertyUtils
import com.facebook.react.utils.findPackageJsonFile
import java.io.File
import kotlin.system.exitProcess
@@ -43,7 +43,6 @@ import org.gradle.internal.jvm.Jvm
class ReactPlugin : Plugin<Project> {
override fun apply(project: Project) {
checkJvmVersion(project)
checkLegacyArchProperty(project)
val extension = project.extensions.create("react", ReactExtension::class.java, project)
// We register a private extension on the rootProject so that project wide configs
@@ -116,30 +115,6 @@ class ReactPlugin : Plugin<Project> {
}
}
private fun checkLegacyArchProperty(project: Project) {
if ((project.hasProperty(PropertyUtils.NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.NEW_ARCH_ENABLED).toString().toBoolean()) ||
(project.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.SCOPED_NEW_ARCH_ENABLED).toString().toBoolean())) {
project.logger.error(
"""
********************************************************************************
WARNING: Setting `newArchEnabled=false` in your `gradle.properties` file is not
supported anymore since React Native 0.82.
You can remove the line from your `gradle.properties` file.
The application will run with the New Architecture enabled by default.
********************************************************************************
"""
.trimIndent())
}
}
/** This function configures Android resources - in this case just the bundle */
private fun configureResources(project: Project, reactExtension: ReactExtension) {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
@@ -296,17 +271,19 @@ class ReactPlugin : Plugin<Project> {
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
// We also need to generate code for C++ Autolinking
val generateAutolinkingNewArchitectureFilesTask =
project.tasks.register(
"generateAutolinkingNewArchitectureFiles",
GenerateAutolinkingNewArchitecturesFileTask::class.java) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
}
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generateAutolinkingNewArchitectureFilesTask)
if (project.isNewArchEnabled(extension)) {
// For New Arch, we also need to generate code for C++ Autolinking
val generateAutolinkingNewArchitectureFilesTask =
project.tasks.register(
"generateAutolinkingNewArchitectureFiles",
GenerateAutolinkingNewArchitecturesFileTask::class.java) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
}
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generateAutolinkingNewArchitectureFilesTask)
}
// We let generateAutolinkingPackageList and generateEntryPoint depend on the preBuild task so
// it's executed before
@@ -13,6 +13,7 @@ import com.android.build.gradle.LibraryExtension
import com.facebook.react.ReactExtension
import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled
import com.facebook.react.utils.ProjectUtils.isHermesEnabled
import com.facebook.react.utils.ProjectUtils.isNewArchEnabled
import java.io.File
import java.net.Inet4Address
import java.net.NetworkInterface
@@ -64,7 +65,10 @@ internal object AgpConfiguratorUtils {
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.buildFeatures.buildConfig = true
ext.defaultConfig.buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
ext.defaultConfig.buildConfigField(
"boolean",
"IS_NEW_ARCHITECTURE_ENABLED",
project.isNewArchEnabled(extension).toString())
ext.defaultConfig.buildConfigField(
"boolean", "IS_HERMES_ENABLED", project.isHermesEnabled.toString())
ext.defaultConfig.buildConfigField(
@@ -11,6 +11,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.facebook.react.ReactExtension
import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures
import com.facebook.react.utils.ProjectUtils.isNewArchEnabled
import java.io.File
import org.gradle.api.Project
@@ -20,6 +21,10 @@ internal object NdkConfiguratorUtils {
project.pluginManager.withPlugin("com.android.application") {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
ext ->
if (!project.isNewArchEnabled(extension)) {
// For Old Arch, we don't need to setup the NDK
return@finalizeDsl
}
// We enable prefab so users can consume .so/headers from ReactAndroid and hermes-engine
// .aar
ext.buildFeatures.prefab = true
@@ -73,19 +78,29 @@ internal object NdkConfiguratorUtils {
extension: ReactExtension,
variant: Variant
) {
// We set some packagingOptions { pickFirst ... } for our users for libraries we own.
variant.packaging.jniLibs.pickFirsts.addAll(
listOf(
// This is the .so provided by FBJNI via prefab
"**/libfbjni.so",
// Those are prefab libraries we distribute via ReactAndroid
// Due to a bug in AGP, they fire a warning on console as both the JNI
// and the prefab .so files gets considered.
"**/libreactnative.so",
"**/libjsi.so",
// AGP will give priority of libc++_shared coming from App modules.
"**/libc++_shared.so",
))
if (!project.isNewArchEnabled(extension)) {
// For Old Arch, we set a pickFirst only on libraries that we know are
// clashing with our direct dependencies (mainly FBJNI and Hermes).
variant.packaging.jniLibs.pickFirsts.addAll(
listOf(
"**/libfbjni.so",
"**/libc++_shared.so",
))
} else {
// We set some packagingOptions { pickFirst ... } for our users for libraries we own.
variant.packaging.jniLibs.pickFirsts.addAll(
listOf(
// This is the .so provided by FBJNI via prefab
"**/libfbjni.so",
// Those are prefab libraries we distribute via ReactAndroid
// Due to a bug in AGP, they fire a warning on console as both the JNI
// and the prefab .so files gets considered.
"**/libreactnative.so",
"**/libjsi.so",
// AGP will give priority of libc++_shared coming from App modules.
"**/libc++_shared.so",
))
}
}
/**
@@ -13,9 +13,11 @@ import com.facebook.react.utils.KotlinStdlibCompatUtils.lowercaseCompat
import com.facebook.react.utils.KotlinStdlibCompatUtils.toBooleanStrictOrNullCompat
import com.facebook.react.utils.PropertyUtils.EDGE_TO_EDGE_ENABLED
import com.facebook.react.utils.PropertyUtils.HERMES_ENABLED
import com.facebook.react.utils.PropertyUtils.NEW_ARCH_ENABLED
import com.facebook.react.utils.PropertyUtils.REACT_NATIVE_ARCHITECTURES
import com.facebook.react.utils.PropertyUtils.SCOPED_EDGE_TO_EDGE_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_HERMES_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_NEW_ARCH_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_REACT_NATIVE_ARCHITECTURES
import com.facebook.react.utils.PropertyUtils.SCOPED_USE_THIRD_PARTY_JSC
import com.facebook.react.utils.PropertyUtils.USE_THIRD_PARTY_JSC
@@ -26,7 +28,12 @@ internal object ProjectUtils {
const val HERMES_FALLBACK = true
internal fun Project.isNewArchEnabled(): Boolean = true
internal fun Project.isNewArchEnabled(extension: ReactExtension): Boolean {
return (project.hasProperty(NEW_ARCH_ENABLED) &&
project.property(NEW_ARCH_ENABLED).toString().toBoolean()) ||
(project.hasProperty(SCOPED_NEW_ARCH_ENABLED) &&
project.property(SCOPED_NEW_ARCH_ENABLED).toString().toBoolean())
}
internal val Project.isHermesEnabled: Boolean
get() =
@@ -27,8 +27,70 @@ class ProjectUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun isNewArchEnabled_alwaysReturnsTrue() {
assertThat(createProject().isNewArchEnabled()).isTrue()
fun isNewArchEnabled_returnsFalseByDefault() {
val project = createProject()
val extension = TestReactExtension(project)
assertThat(createProject().isNewArchEnabled(extension)).isFalse()
}
@Test
fun isNewArchEnabled_withDisabled_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("newArchEnabled", "false")
val extension = TestReactExtension(project)
assertThat(project.isNewArchEnabled(extension)).isFalse()
}
@Test
fun isNewArchEnabled_withEnabled_returnsTrue() {
val project = createProject()
project.extensions.extraProperties.set("newArchEnabled", "true")
val extension = TestReactExtension(project)
assertThat(project.isNewArchEnabled(extension)).isTrue()
}
@Test
fun isNewArchEnabled_withInvalid_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("newArchEnabled", "¯\\_(ツ)_/¯")
val extension = TestReactExtension(project)
assertThat(project.isNewArchEnabled(extension)).isFalse()
}
@Test
fun isNewArchEnabled_withRNVersion0_returnFalse() {
val project = createProject()
val extension = TestReactExtension(project)
File(tempFolder.root, "package.json").apply {
writeText(
// language=json
"""
{
"version": "0.73.0"
}
"""
.trimIndent())
}
extension.reactNativeDir.set(tempFolder.root)
assertThat(project.isNewArchEnabled(extension)).isFalse()
}
@Test
fun isNewArchEnabled_withRNVersion1000_returnFalse() {
val project = createProject()
val extension = TestReactExtension(project)
File(tempFolder.root, "package.json").apply {
writeText(
// language=json
"""
{
"version": "1000.0.0"
}
"""
.trimIndent())
}
extension.reactNativeDir.set(tempFolder.root)
assertThat(project.isNewArchEnabled(extension)).isFalse()
}
@Test
-4
View File
@@ -42,14 +42,10 @@ const ErrorUtils = {
return _globalHandler;
},
reportError(error: mixed): void {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
_globalHandler && _globalHandler(error, false);
},
reportFatalError(error: mixed): void {
// NOTE: This has an untyped call site in Metro.
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
_globalHandler && _globalHandler(error, true);
},
applyWithGuard<TArgs: $ReadOnlyArray<mixed>, TOut>(
@@ -67,7 +67,7 @@
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
"@react-native/babel-plugin-codegen": "0.82.0-main",
"babel-plugin-syntax-hermes-parser": "0.30.0",
"babel-plugin-syntax-hermes-parser": "0.29.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
+14 -17
View File
@@ -38,6 +38,9 @@ function isFirstParty(fileName) {
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
const loose = true;
// For Static Hermes testing (experimental), the hermes-canary transformProfile
// is used to enable regenerator (and some related lowering passes) because SH
// requires more Babel lowering than Hermes temporarily.
const getPreset = (src, options) => {
const transformProfile =
(options && options.unstable_transformProfile) || 'default';
@@ -45,14 +48,6 @@ const getPreset = (src, options) => {
const isHermesCanary = transformProfile === 'hermes-canary';
const isHermes = isHermesStable || isHermesCanary;
// We enable regenerator for !isHermes. Additionally, in dev mode we also
// enable regenerator for the time being because Static Hermes doesn't yet
// support debugging native generators. However, some apps have native
// generators in release mode because it has already yielded perf wins. The
// next release of Static Hermes will close this gap, so this won't be
// permanent.
const enableRegenerator = !isHermes || options.dev;
const isNull = src == null;
const hasClass = isNull || src.indexOf('class') !== -1;
@@ -116,8 +111,8 @@ const getPreset = (src, options) => {
extraPlugins.push([
require('@babel/plugin-transform-named-capturing-groups-regex'),
]);
// Needed for regenerator
if (isHermes && enableRegenerator) {
// Needed for regenerator for hermes-canary
if (isHermesCanary) {
extraPlugins.push([
require('@babel/plugin-transform-optional-catch-binding'),
]);
@@ -150,15 +145,17 @@ const getPreset = (src, options) => {
) {
extraPlugins.push([require('@babel/plugin-transform-react-display-name')]);
}
// This is also needed for regenerator
if (enableRegenerator && (isNull || src.indexOf('?.') !== -1)) {
// Check !isHermesStable because this is needed for regenerator for
// hermes-canary
if (!isHermesStable && (isNull || src.indexOf('?.') !== -1)) {
extraPlugins.push([
require('@babel/plugin-transform-optional-chaining'),
{loose: true},
]);
}
// This is also needed for regenerator
if (enableRegenerator && (isNull || src.indexOf('??') !== -1)) {
// Check !isHermesStable because this is needed for regenerator for
// hermes-canary
if (!isHermesStable && (isNull || src.indexOf('??') !== -1)) {
extraPlugins.push([
require('@babel/plugin-transform-nullish-coalescing-operator'),
{loose: true},
@@ -186,7 +183,7 @@ const getPreset = (src, options) => {
extraPlugins.push([require('@babel/plugin-transform-react-jsx-self')]);
}
if (isHermes && enableRegenerator) {
if (isHermesCanary) {
const hasForOf =
isNull || (src.indexOf('for') !== -1 && src.indexOf('of') !== -1);
if (hasForOf) {
@@ -206,11 +203,11 @@ const getPreset = (src, options) => {
require('@babel/plugin-transform-runtime'),
{
helpers: true,
regenerator: enableRegenerator,
regenerator: !isHermesStable,
...(isVersion && {version: options.enableBabelRuntime}),
},
]);
} else if (isHermes && enableRegenerator) {
} else if (isHermesCanary) {
extraPlugins.push([require('@babel/plugin-transform-regenerator')]);
}
@@ -28,7 +28,7 @@
"dependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.82.0-main",
"hermes-parser": "0.30.0",
"hermes-parser": "0.29.1",
"nullthrows": "^1.1.1"
},
"peerDependencies": {
+3 -5
View File
@@ -29,10 +29,8 @@
"lib"
],
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/parser": "^7.25.3",
"glob": "^7.1.1",
"hermes-parser": "0.30.0",
"hermes-parser": "0.29.1",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"yargs": "^17.6.2"
@@ -45,9 +43,9 @@
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
"@babel/plugin-transform-optional-chaining": "^7.24.8",
"@babel/preset-env": "^7.25.3",
"hermes-estree": "0.30.0",
"hermes-estree": "0.29.1",
"micromatch": "^4.0.4",
"prettier": "3.6.2",
"prettier": "2.8.8",
"rimraf": "^3.0.2"
},
"peerDependencies": {
@@ -487,7 +487,7 @@ describe('buildSchemaFromConfigType', () => {
describe('when buildModuleSchema returns null', () => {
it('throws an error', () => {
// $FlowFixMe[incompatible-call] - This is to test an invariant
// $FlowIgnore[incompatible-call] - This is to test an invariant
buildModuleSchemaMock.mockReturnValueOnce(null);
expect(() =>
@@ -10,9 +10,9 @@
'use strict';
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
const flowSnaps = require('../../../../src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap');
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
const tsSnaps = require('../../../../src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap');
const flowFixtures = require('../../flow/components/__test_fixtures__/fixtures.js');
const tsFixtures = require('../../typescript/components/__test_fixtures__/fixtures.js');
@@ -10,9 +10,9 @@
'use strict';
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
const flowSnaps = require('../../../../src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap');
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
const tsSnaps = require('../../../../src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap');
const flowFixtures = require('../../flow/modules/__test_fixtures__/fixtures.js');
const tsFixtures = require('../../typescript/modules/__test_fixtures__/fixtures.js');
@@ -28,8 +28,6 @@ export function alertWithArgs(
args,
emptyCallback,
// $FlowFixMe[incompatible-call] - Mismatched platform interfaces.
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
callback || emptyCallback,
);
}
@@ -94,8 +94,6 @@ const _combineCallbacks = function (
if (callback && config.onComplete) {
return (...args: Array<EndResult>) => {
config.onComplete && config.onComplete(...args);
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
callback && callback(...args);
};
} else {
-2
View File
@@ -91,8 +91,6 @@ const EasingStatic = {
* http://cubic-bezier.com/#.42,0,1,1
*/
ease(t: number): number {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (!ease) {
ease = EasingStatic.bezier(0.42, 0, 1, 1);
}
@@ -48,7 +48,6 @@ const SUPPORTED_STYLES: {[string]: true} = {
borderStartStartRadius: true,
elevation: true,
opacity: true,
filter: true,
transform: true,
zIndex: true,
/* ios styles */
@@ -1,133 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {Animated, View, useAnimatedValue} from 'react-native';
function MyApp() {
return (
<View
style={[
{
width: 100,
height: 100,
opacity: 1,
},
]}
/>
);
}
function MyAnimatedApp() {
return (
<Animated.View
style={[
{
width: 100,
height: 100,
opacity: 1,
},
]}
/>
);
}
function MyAnimatedAppWithAnimation() {
const opacity = useAnimatedValue(1);
return (
<Animated.View
style={[
{
width: 100,
height: 100,
opacity,
},
]}
/>
);
}
function MyAnimatedAppWithNativeAnimation() {
const opacity = useAnimatedValue(1, {useNativeDriver: true});
return (
<Animated.View
style={[
{
width: 100,
height: 100,
opacity,
},
]}
/>
);
}
const ARGS = [1, 10, 100];
let root = Fantom.createRoot();
let element: React.MixedElement;
function renderElement() {
Fantom.runTask(() => root.render(element));
}
function getOptions(
numberOfComponents: number,
Component: React.ComponentType<{}>,
): Fantom.BenchmarkTestOptions {
return {
beforeAll: () => {
element = (
<>
{Array.from({length: numberOfComponents}, (_, i) => (
<Component key={i} />
))}
</>
);
},
beforeEach: () => {
Fantom.runTask(() => root.render(<></>));
},
};
}
Fantom.unstable_benchmark
.suite('Animated')
.test.each(
ARGS,
n => `render ${n} views`,
renderElement,
n => getOptions(n, MyApp),
)
.test.each(
ARGS,
n => `render ${n} animated views (without animations set up)`,
renderElement,
n => getOptions(n, MyAnimatedApp),
)
.test.each(
ARGS,
n =>
`render ${n} animated views (with a single animation set up - JS driven)`,
renderElement,
n => getOptions(n, MyAnimatedAppWithAnimation),
)
.test.each(
ARGS,
n =>
`render ${n} animated views (with a single animation set up - native driven)`,
renderElement,
n => getOptions(n, MyAnimatedAppWithNativeAnimation),
);
@@ -77,13 +77,15 @@ test('moving box by 100 points', () => {
// Animation is completed now. C++ Animated will commit the final position to the shadow tree.
if (ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
// TODO(T232605345): this shouldn't be neccessary once we fix Android's race condition.
expect(viewElement.getBoundingClientRect().x).toBe(100);
// TODO(T223344928): this shouldn't be neccessary
Fantom.runWorkLoop();
expect(viewElement.getBoundingClientRect().x).toBe(100);
} else {
expect(viewElement.getBoundingClientRect().x).toBe(0);
Fantom.runWorkLoop(); // Animated still schedules a React state update for synchronisation to shadow tree
expect(viewElement.getBoundingClientRect().x).toBe(100);
}
expect(viewElement.getBoundingClientRect().x).toBe(100);
});
test('animation driven by onScroll event', () => {
@@ -146,73 +148,9 @@ test('animation driven by onScroll event', () => {
expect(transform.translateY).toBeCloseTo(100, 0.001);
// TODO(T232605345): The following two lines won't be necessary once race condition on Android is fixed
expect(viewElement.getBoundingClientRect().y).toBe(0);
Fantom.runWorkLoop();
expect(viewElement.getBoundingClientRect().y).toBe(100);
});
test('animation driven by onScroll event when animated view is unmounted', () => {
const scrollViewRef = createRef<HostInstance>();
component PressableWithNativeDriver(mountAnimatedView: boolean) {
const currScroll = useAnimatedValue(0);
return (
<View style={{flex: 1}}>
{mountAnimatedView ? (
<Animated.View
style={{
position: 'absolute',
width: 10,
height: 10,
transform: [{translateY: currScroll}],
}}
/>
) : null}
<Animated.ScrollView
ref={scrollViewRef}
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: currScroll,
},
},
},
],
{useNativeDriver: true},
)}>
<View style={{height: 1000, width: 100}} />
</Animated.ScrollView>
</View>
);
}
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<PressableWithNativeDriver mountAnimatedView={true} />);
});
Fantom.runTask(() => {
root.render(<PressableWithNativeDriver mountAnimatedView={false} />);
});
const scrollViewelement = ensureInstance(
scrollViewRef.current,
ReactNativeElement,
);
Fantom.scrollTo(scrollViewelement, {
x: 0,
y: 50,
});
Fantom.runWorkLoop();
});
test('animated opacity', () => {
let _opacity;
let _opacityAnimation;
@@ -335,21 +273,22 @@ test('moving box by 50 points with offset 10', () => {
).toBeCloseTo(60, 0.001);
if (ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
// TODO(T232605345): The following line won't be necessary once race condition on Android is fixed.
Fantom.runWorkLoop();
expect(root.getRenderedOutput({props: ['transform']}).toJSX()).toEqual(
<rn-view transform='[{"translateX": 60.000000}]' />,
);
// TODO(T223344928): this shouldn't be neccessary
Fantom.runWorkLoop();
} else {
expect(root.getRenderedOutput({props: ['transform']}).toJSX()).toEqual(
<rn-view transform="[]" />,
);
Fantom.runWorkLoop(); // Animated still schedules a React state update for synchronisation to shadow tree
expect(root.getRenderedOutput({props: ['transform']}).toJSX()).toEqual(
<rn-view transform='[{"translateX": 60.000000}]' />, // // must include offset.
);
}
expect(root.getRenderedOutput({props: ['transform']}).toJSX()).toEqual(
<rn-view transform='[{"translateX": 60.000000}]' />, // // must include offset.
);
expect(finishValue?.finished).toBe(true);
expect(finishValue?.value).toBe(50); // must not include offset.
expect(finishValue?.offset).toBe(10);
@@ -436,6 +375,9 @@ describe('Value.flattenOffset', () => {
Fantom.unstable_getDirectManipulationProps(viewElement).transform[0];
expect(transform.translateY).toBeCloseTo(40, 0.001);
// TODO(T223344928): this shouldn't be neccessary with cxxNativeAnimatedRemoveJsSync:true
Fantom.runWorkLoop();
});
});
@@ -532,6 +474,9 @@ describe('Value.extractOffset', () => {
// `extractOffset` resets value back to 0.
// Previously we set offset to 35. The final value is 35.
expect(transform.translateY).toBeCloseTo(35, 0.001);
// TODO(T223344928): this shouldn't be neccessary with cxxNativeAnimatedRemoveJsSync:true
Fantom.runWorkLoop();
});
});
@@ -725,25 +670,27 @@ test('Animated.sequence', () => {
Fantom.unstable_getDirectManipulationProps(element).transform[0].translateY,
).toBeCloseTo(-16, 0.001);
Fantom.runWorkLoop(); // React update to sync end state of 1st timing animation in sequence
if (!ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
Fantom.runWorkLoop(); // React update to sync end state of 1st timing animation in sequence
}
expect(_isSequenceFinished).toBe(false);
expect(element.getBoundingClientRect().y).toBe(-16);
Fantom.runWorkLoop(); // React render
expect(
// $FlowFixMe[incompatible-use]
Fantom.unstable_getDirectManipulationProps(element).transform[0].translateY,
).toBeCloseTo(0, 0.001);
if (ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
// TODO(T232605345): The following two lines won't be necessary once race condition on Android is fixed
expect(element.getBoundingClientRect().y).toBe(-16);
Fantom.runWorkLoop();
expect(element.getBoundingClientRect().y).toBe(0);
} else {
expect(element.getBoundingClientRect().y).toBe(-16);
if (!ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
Fantom.runWorkLoop(); // React update to sync end state of 2nd timing animation in sequence
expect(element.getBoundingClientRect().y).toBe(0);
}
expect(element.getBoundingClientRect().y).toBe(0);
if (ReactNativeFeatureFlags.cxxNativeAnimatedRemoveJsSync()) {
expect(_isSequenceFinished).toBe(false);
Fantom.runWorkLoop();
}
expect(_isSequenceFinished).toBe(true);
});
@@ -21,11 +21,11 @@ function mockQueueMicrotask() {
let queueMicrotask;
beforeEach(() => {
queueMicrotask = global.queueMicrotask;
// $FlowFixMe[cannot-write]
// $FlowIgnore[cannot-write]
global.queueMicrotask = process.nextTick;
});
afterEach(() => {
// $FlowFixMe[cannot-write]
// $FlowIgnore[cannot-write]
global.queueMicrotask = queueMicrotask;
});
}
@@ -261,7 +261,7 @@ describe('Animated', () => {
expect(console.warn).toBeCalledWith(
'Animated: `useNativeDriver` was not specified. This is a required option and must be explicitly set to `true` or `false`',
);
// $FlowFixMe[prop-missing]
// $FlowIssue[prop-missing]
console.warn.mockRestore();
});
@@ -41,9 +41,9 @@ let startNativeAnimationNextId = 1;
// Once an animation has been stopped or finished its course, it will
// not be reused.
export default class Animation {
_nativeID: ?number;
_onEnd: ?EndCallback;
_useNativeDriver: boolean;
#nativeID: ?number;
#onEnd: ?EndCallback;
#useNativeDriver: boolean;
__active: boolean;
__isInteraction: boolean;
@@ -52,10 +52,10 @@ export default class Animation {
__debugID: ?string;
constructor(config: AnimationConfig) {
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this.#useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this.__active = false;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
this.__isInteraction = config.isInteraction ?? !this.#useNativeDriver;
this.__isLooping = config.isLooping;
this.__iterations = config.iterations ?? 1;
if (__DEV__) {
@@ -70,7 +70,7 @@ export default class Animation {
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
if (!this._useNativeDriver && animatedValue.__isNative === true) {
if (!this.#useNativeDriver && animatedValue.__isNative === true) {
throw new Error(
'Attempting to run JS driven animation on animated node ' +
'that has been moved to "native" earlier by starting an ' +
@@ -78,13 +78,13 @@ export default class Animation {
);
}
this._onEnd = onEnd;
this.#onEnd = onEnd;
this.__active = true;
}
stop(): void {
if (this._nativeID != null) {
const nativeID = this._nativeID;
if (this.#nativeID != null) {
const nativeID = this.#nativeID;
const identifier = `${nativeID}:stopAnimation`;
try {
// This is only required when singleOpBatching is used, as otherwise
@@ -123,7 +123,7 @@ export default class Animation {
}
__startAnimationIfNative(animatedValue: AnimatedValue): boolean {
if (!this._useNativeDriver) {
if (!this.#useNativeDriver) {
return false;
}
@@ -135,9 +135,9 @@ export default class Animation {
try {
const config = this.__getNativeAnimationConfig();
animatedValue.__makeNative(config.platformConfig);
this._nativeID = NativeAnimatedHelper.generateNewAnimationId();
this.#nativeID = NativeAnimatedHelper.generateNewAnimationId();
NativeAnimatedHelper.API.startAnimatingNode(
this._nativeID,
this.#nativeID,
animatedValue.__getNativeTag(),
config,
result => {
@@ -185,9 +185,9 @@ export default class Animation {
* callback will never be called more than once.
*/
__notifyAnimationEnd(result: EndResult): void {
const callback = this._onEnd;
const callback = this.#onEnd;
if (callback != null) {
this._onEnd = null;
this.#onEnd = null;
callback(result);
}
}
@@ -49,8 +49,6 @@ export type TimingAnimationConfigSingle = $ReadOnly<{
let _easeInOut;
function easeInOut() {
/* $FlowFixMe[constant-condition] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/1v97vimq. */
if (!_easeInOut) {
const Easing = require('../Easing').default;
_easeInOut = Easing.inOut(Easing.ease);
@@ -60,12 +60,12 @@ function processColor(
}
if (isRgbaValue(color)) {
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
return (color: RgbaValue);
}
let normalizedColor: ?ProcessedColorValue = normalizeColor(
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
(color: ColorValue),
);
if (normalizedColor === undefined || normalizedColor === null) {
@@ -125,7 +125,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
let value: RgbaValue | RgbaAnimatedValue | ColorValue =
valueIn ?? defaultColor;
if (isRgbaAnimatedValue(value)) {
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
const rgbaAnimatedValue: RgbaAnimatedValue = (value: RgbaAnimatedValue);
this.r = rgbaAnimatedValue.r;
this.g = rgbaAnimatedValue.g;
@@ -133,14 +133,14 @@ export default class AnimatedColor extends AnimatedWithChildren {
this.a = rgbaAnimatedValue.a;
} else {
const processedColor: RgbaValue | NativeColorValue =
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
processColor((value: ColorValue | RgbaValue)) ?? defaultColor;
let initColor: RgbaValue = defaultColor;
if (isRgbaValue(processedColor)) {
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
initColor = (processedColor: RgbaValue);
} else {
// $FlowFixMe[incompatible-cast] - Type is verified above
// $FlowIgnore[incompatible-cast] - Type is verified above
this.nativeColor = (processedColor: NativeColorValue);
}
@@ -170,7 +170,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
processColor(value) ?? defaultColor;
this._withSuspendedCallbacks(() => {
if (isRgbaValue(processedColor)) {
// $FlowFixMe[incompatible-type] - Type is verified above
// $FlowIgnore[incompatible-type] - Type is verified above
const rgbaValue: RgbaValue = processedColor;
this.r.setValue(rgbaValue.r);
this.g.setValue(rgbaValue.g);
@@ -181,7 +181,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
shouldUpdateNodeConfig = true;
}
} else {
// $FlowFixMe[incompatible-type] - Type is verified above
// $FlowIgnore[incompatible-type] - Type is verified above
const nativeColor: NativeColorValue = processedColor;
if (this.nativeColor !== nativeColor) {
this.nativeColor = nativeColor;
@@ -224,7 +224,7 @@ function createStringInterpolation(
outputRange.every(output =>
output.components.every(
(component, i) =>
// $FlowFixMe[invalid-compare]
// $FlowIgnoreMe[invalid-compare]
typeof component === 'number' || component === firstOutput[i],
),
),
@@ -235,9 +235,9 @@ function createStringInterpolation(
const numericComponents: $ReadOnlyArray<$ReadOnlyArray<number>> =
outputRange.map(output =>
isColor
? // $FlowFixMe[incompatible-type]
? // $FlowIgnoreMe[incompatible-call]
output.components
: // $FlowFixMe[incompatible-call]
: // $FlowIgnoreMe[incompatible-call]
output.components.filter(c => typeof c === 'number'),
);
const interpolations = numericComponents[0].map((_, i) =>
@@ -393,7 +393,7 @@ export default class AnimatedInterpolation<
let outputRange = this._config.outputRange;
let outputType = null;
if (typeof outputRange[0] === 'string') {
// $FlowFixMe[incompatible-cast]
// $FlowIgnoreMe[incompatible-cast]
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
const processedColor = processColor(value);
if (typeof processedColor === 'number') {
@@ -28,7 +28,7 @@ let _assertNativeAnimatedModule: ?() => void = () => {
};
export default class AnimatedNode {
_listeners: Map<string, ValueListenerCallback>;
#listeners: Map<string, ValueListenerCallback>;
_platformConfig: ?PlatformConfig = undefined;
@@ -38,7 +38,7 @@ export default class AnimatedNode {
...
}>,
) {
this._listeners = new Map();
this.#listeners = new Map();
if (__DEV__) {
this.__debugID = config?.debugID;
}
@@ -85,7 +85,7 @@ export default class AnimatedNode {
*/
addListener(callback: (value: any) => mixed): string {
const id = String(_uniqueId++);
this._listeners.set(id, callback);
this.#listeners.set(id, callback);
return id;
}
@@ -96,7 +96,7 @@ export default class AnimatedNode {
* See https://reactnative.dev/docs/animatedvalue#removelistener
*/
removeListener(id: string): void {
this._listeners.delete(id);
this.#listeners.delete(id);
}
/**
@@ -105,11 +105,11 @@ export default class AnimatedNode {
* See https://reactnative.dev/docs/animatedvalue#removealllisteners
*/
removeAllListeners(): void {
this._listeners.clear();
this.#listeners.clear();
}
hasListeners(): boolean {
return this._listeners.size > 0;
return this.#listeners.size > 0;
}
__onAnimatedValueUpdateReceived(value: number, offset: number): void {
@@ -118,7 +118,7 @@ export default class AnimatedNode {
__callListeners(value: number): void {
const event = {value};
this._listeners.forEach(listener => {
this.#listeners.forEach(listener => {
listener(event);
});
}
@@ -21,7 +21,7 @@ const MAX_DEPTH = 5;
export function isPlainObject(
value: mixed,
/* $FlowFixMe[incompatible-type-guard] - Flow does not know that the prototype
/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype
and ReactElement checks preserve the type refinement of `value`. */
): value is $ReadOnly<{[string]: mixed}> {
return (
@@ -82,7 +82,7 @@ function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any {
}
export default class AnimatedObject extends AnimatedWithChildren {
_nodes: $ReadOnlyArray<AnimatedNode>;
#nodes: $ReadOnlyArray<AnimatedNode>;
_value: mixed;
/**
@@ -106,7 +106,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodes = nodes;
this.#nodes = nodes;
this._value = value;
}
@@ -117,7 +117,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
}
__getValueWithStaticObject(staticObject: mixed): any {
const nodes = this._nodes;
const nodes = this.#nodes;
let index = 0;
// NOTE: We can depend on `this._value` and `staticObject` sharing a
// structure because of `useAnimatedPropsMemo`.
@@ -131,7 +131,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
}
__attach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
@@ -140,7 +140,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
}
__detach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
@@ -149,7 +149,7 @@ export default class AnimatedObject extends AnimatedWithChildren {
}
__makeNative(platformConfig: ?PlatformConfig): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
@@ -92,11 +92,11 @@ function createAnimatedProps(
}
export default class AnimatedProps extends AnimatedNode {
_callback: () => void;
_nodeKeys: $ReadOnlyArray<string>;
_nodes: $ReadOnlyArray<AnimatedNode>;
_props: {[string]: mixed};
_target: ?TargetView = null;
#callback: () => void;
#nodeKeys: $ReadOnlyArray<string>;
#nodes: $ReadOnlyArray<AnimatedNode>;
#props: {[string]: mixed};
#target: ?TargetView = null;
constructor(
inputProps: {[string]: mixed},
@@ -106,19 +106,19 @@ export default class AnimatedProps extends AnimatedNode {
) {
super(config);
const [nodeKeys, nodes, props] = createAnimatedProps(inputProps, allowlist);
this._nodeKeys = nodeKeys;
this._nodes = nodes;
this._props = props;
this._callback = callback;
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
this.#props = props;
this.#callback = callback;
}
__getValue(): Object {
const props: {[string]: mixed} = {};
const keys = Object.keys(this._props);
const keys = Object.keys(this.#props);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._props[key];
const value = this.#props[key];
if (value instanceof AnimatedNode) {
props[key] = value.__getValue();
@@ -143,7 +143,7 @@ export default class AnimatedProps extends AnimatedNode {
const keys = Object.keys(staticProps);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this._props[key];
const maybeNode = this.#props[key];
if (key === 'style') {
const staticStyle = staticProps.style;
@@ -176,10 +176,10 @@ export default class AnimatedProps extends AnimatedNode {
__getNativeAnimatedEventTuples(): $ReadOnlyArray<[string, AnimatedEvent]> {
const tuples = [];
const keys = Object.keys(this._props);
const keys = Object.keys(this.#props);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._props[key];
const value = this.#props[key];
if (value instanceof AnimatedEvent && value.__isNative) {
tuples.push([key, value]);
@@ -192,8 +192,8 @@ export default class AnimatedProps extends AnimatedNode {
__getAnimatedValue(): Object {
const props: {[string]: mixed} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
const nodeKeys = this.#nodeKeys;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
@@ -204,7 +204,7 @@ export default class AnimatedProps extends AnimatedNode {
}
__attach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
@@ -213,12 +213,12 @@ export default class AnimatedProps extends AnimatedNode {
}
__detach(): void {
if (this.__isNative && this._target != null) {
this.#disconnectAnimatedView(this._target);
if (this.__isNative && this.#target != null) {
this.#disconnectAnimatedView(this.#target);
}
this._target = null;
this.#target = null;
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
@@ -228,11 +228,11 @@ export default class AnimatedProps extends AnimatedNode {
}
update(): void {
this._callback();
this.#callback();
}
__makeNative(platformConfig: ?PlatformConfig): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
@@ -246,19 +246,19 @@ export default class AnimatedProps extends AnimatedNode {
// where it will be needed to traverse the graph of attached values.
super.__setPlatformConfig(platformConfig);
if (this._target != null) {
this.#connectAnimatedView(this._target);
if (this.#target != null) {
this.#connectAnimatedView(this.#target);
}
}
}
setNativeView(instance: TargetViewInstance): void {
if (this._target?.instance === instance) {
if (this.#target?.instance === instance) {
return;
}
this._target = {instance, connectedViewTag: null};
this.#target = {instance, connectedViewTag: null};
if (this.__isNative) {
this.#connectAnimatedView(this._target);
this.#connectAnimatedView(this.#target);
}
}
@@ -306,8 +306,8 @@ export default class AnimatedProps extends AnimatedNode {
const platformConfig = this.__getPlatformConfig();
const propsConfig: {[string]: number} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
const nodeKeys = this.#nodeKeys;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
@@ -325,8 +325,8 @@ export default class AnimatedProps extends AnimatedNode {
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowFixMe[method-unbinding]
// $FlowIgnore[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowFixMe[method-unbinding]
// $FlowIgnore[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
@@ -82,10 +82,10 @@ function createAnimatedStyle(
}
export default class AnimatedStyle extends AnimatedWithChildren {
_originalStyleForWeb: ?mixed;
_nodeKeys: $ReadOnlyArray<string>;
_nodes: $ReadOnlyArray<AnimatedNode>;
_style: {[string]: mixed};
#originalStyleForWeb: ?mixed;
#nodeKeys: $ReadOnlyArray<string>;
#nodes: $ReadOnlyArray<AnimatedNode>;
#style: {[string]: mixed};
/**
* Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances.
@@ -118,12 +118,12 @@ export default class AnimatedStyle extends AnimatedWithChildren {
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodeKeys = nodeKeys;
this._nodes = nodes;
this._style = style;
this.#nodeKeys = nodeKeys;
this.#nodes = nodes;
this.#style = style;
if ((Platform.OS as string) === 'web') {
// $FlowFixMe[cannot-write] - Intentional shadowing.
// $FlowIgnore[cannot-write] - Intentional shadowing.
this.__getValueForStyle = resultStyle => [
originalStyleForWeb,
resultStyle,
@@ -134,10 +134,10 @@ export default class AnimatedStyle extends AnimatedWithChildren {
__getValue(): FlatStyleForWeb<FlatStyle> | FlatStyle {
const style: {[string]: mixed} = {};
const keys = Object.keys(this._style);
const keys = Object.keys(this.#style);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._style[key];
const value = this.#style[key];
if (value instanceof AnimatedNode) {
style[key] = value.__getValue();
@@ -166,7 +166,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
const keys = Object.keys(style);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this._style[key];
const maybeNode = this.#style[key];
if (key === 'transform' && maybeNode instanceof AnimatedTransform) {
style[key] = maybeNode.__getValueWithStaticTransforms(
@@ -185,8 +185,8 @@ export default class AnimatedStyle extends AnimatedWithChildren {
__getAnimatedValue(): Object {
const style: {[string]: mixed} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
const nodeKeys = this.#nodeKeys;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
@@ -197,7 +197,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
}
__attach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
@@ -206,7 +206,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
}
__detach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
@@ -215,7 +215,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
}
__makeNative(platformConfig: ?PlatformConfig) {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
@@ -227,8 +227,8 @@ export default class AnimatedStyle extends AnimatedWithChildren {
const platformConfig = this.__getPlatformConfig();
const styleConfig: {[string]: ?number} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
const nodeKeys = this.#nodeKeys;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
@@ -249,8 +249,8 @@ export default class AnimatedStyle extends AnimatedWithChildren {
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowFixMe[method-unbinding]
// $FlowIgnore[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowFixMe[method-unbinding]
// $FlowIgnore[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
@@ -47,7 +47,7 @@ function flatAnimatedNodes(
export default class AnimatedTransform extends AnimatedWithChildren {
// NOTE: For potentially historical reasons, some operations only operate on
// the first level of AnimatedNode instances. This optimizes that bevavior.
_nodes: $ReadOnlyArray<AnimatedNode>;
#nodes: $ReadOnlyArray<AnimatedNode>;
_transforms: $ReadOnlyArray<Transform<>>;
@@ -74,12 +74,12 @@ export default class AnimatedTransform extends AnimatedWithChildren {
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodes = nodes;
this.#nodes = nodes;
this._transforms = transforms;
}
__makeNative(platformConfig: ?PlatformConfig) {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
@@ -112,7 +112,7 @@ export default class AnimatedTransform extends AnimatedWithChildren {
}
__attach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
@@ -121,7 +121,7 @@ export default class AnimatedTransform extends AnimatedWithChildren {
}
__detach(): void {
const nodes = this._nodes;
const nodes = this.#nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
@@ -85,8 +85,8 @@ function _executeAsAnimatedBatch(id: string, operation: () => void) {
* See https://reactnative.dev/docs/animatedvalue
*/
export default class AnimatedValue extends AnimatedWithChildren {
_listenerCount: number;
_updateSubscription: ?EventSubscription;
#listenerCount: number;
#updateSubscription: ?EventSubscription;
_value: number;
_startingValue: number;
@@ -100,8 +100,8 @@ export default class AnimatedValue extends AnimatedWithChildren {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
this._listenerCount = 0;
this._updateSubscription = null;
this.#listenerCount = 0;
this.#updateSubscription = null;
this._startingValue = this._value = value;
this._offset = 0;
@@ -127,38 +127,38 @@ export default class AnimatedValue extends AnimatedWithChildren {
__makeNative(platformConfig: ?PlatformConfig): void {
super.__makeNative(platformConfig);
if (this._listenerCount > 0) {
this.__ensureUpdateSubscriptionExists();
if (this.#listenerCount > 0) {
this.#ensureUpdateSubscriptionExists();
}
}
addListener(callback: (value: any) => mixed): string {
const id = super.addListener(callback);
this._listenerCount++;
this.#listenerCount++;
if (this.__isNative) {
this.__ensureUpdateSubscriptionExists();
this.#ensureUpdateSubscriptionExists();
}
return id;
}
removeListener(id: string): void {
super.removeListener(id);
this._listenerCount--;
if (this.__isNative && this._listenerCount === 0) {
this._updateSubscription?.remove();
this.#listenerCount--;
if (this.__isNative && this.#listenerCount === 0) {
this.#updateSubscription?.remove();
}
}
removeAllListeners(): void {
super.removeAllListeners();
this._listenerCount = 0;
this.#listenerCount = 0;
if (this.__isNative) {
this._updateSubscription?.remove();
this.#updateSubscription?.remove();
}
}
__ensureUpdateSubscriptionExists(): void {
if (this._updateSubscription != null) {
#ensureUpdateSubscriptionExists(): void {
if (this.#updateSubscription != null) {
return;
}
const nativeTag = this.__getNativeTag();
@@ -173,13 +173,13 @@ export default class AnimatedValue extends AnimatedWithChildren {
},
);
this._updateSubscription = {
this.#updateSubscription = {
remove: () => {
// Only this function assigns to `this.#updateSubscription`.
if (this._updateSubscription == null) {
if (this.#updateSubscription == null) {
return;
}
this._updateSubscription = null;
this.#updateSubscription = null;
subscription.remove();
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(nativeTag);
},
@@ -31,7 +31,7 @@
void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
{
RCTEnableTurboModule(YES);
RCTEnableTurboModule(turboModuleEnabled);
#if DEBUG
// Disable idle timer in dev builds to avoid putting application in background and complicating
@@ -43,12 +43,15 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
UIView *
RCTAppSetupDefaultRootView(RCTBridge *bridge, NSString *moduleName, NSDictionary *initialProperties, BOOL fabricEnabled)
{
id<RCTSurfaceProtocol> surface = [[RCTFabricSurface alloc] initWithBridge:bridge
moduleName:moduleName
initialProperties:initialProperties];
UIView *rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
[surface start];
return rootView;
if (fabricEnabled) {
id<RCTSurfaceProtocol> surface = [[RCTFabricSurface alloc] initWithBridge:bridge
moduleName:moduleName
initialProperties:initialProperties];
UIView *rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
[surface start];
return rootView;
}
return [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
NSArray<NSString *> *RCTAppSetupUnstableModulesRequiringMainQueueSetup(id<RCTDependencyProvider> dependencyProvider)
@@ -57,7 +57,8 @@
moduleName:(NSString *)moduleName
initProps:(NSDictionary *)initProps
{
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, YES);
BOOL enableFabric = self.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
rootView.backgroundColor = [UIColor systemBackgroundColor];
@@ -106,22 +107,22 @@
- (BOOL)newArchEnabled
{
return YES;
return RCTIsNewArchEnabled();
}
- (BOOL)bridgelessEnabled
{
return YES;
return self.newArchEnabled;
}
- (BOOL)fabricEnabled
{
return YES;
return self.newArchEnabled;
}
- (BOOL)turboModuleEnabled
{
return YES;
return self.newArchEnabled;
}
- (Class)getModuleClassFromName:(const char *)name
@@ -52,12 +52,17 @@ using namespace facebook::react;
self.delegate = delegate;
[self _setUpFeatureFlags:releaseLevel];
auto newArchEnabled = [self newArchEnabled];
auto fabricEnabled = [self fabricEnabled];
[RCTColorSpaceUtils applyDefaultColorSpace:[self defaultColorSpace]];
RCTEnableTurboModule(YES);
RCTEnableTurboModule([self turboModuleEnabled]);
self.rootViewFactory = [self createRCTRootViewFactory];
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
if (newArchEnabled || fabricEnabled) {
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}
}
return self;
@@ -121,22 +126,37 @@ using namespace facebook::react;
- (BOOL)newArchEnabled
{
return YES;
if ([_delegate respondsToSelector:@selector(newArchEnabled)]) {
return _delegate.newArchEnabled;
}
return RCTIsNewArchEnabled();
}
- (BOOL)fabricEnabled
{
return YES;
if ([_delegate respondsToSelector:@selector(fabricEnabled)]) {
return _delegate.fabricEnabled;
}
return [self newArchEnabled];
}
- (BOOL)turboModuleEnabled
{
return YES;
if ([_delegate respondsToSelector:@selector(turboModuleEnabled)]) {
return _delegate.turboModuleEnabled;
}
return [self newArchEnabled];
}
- (BOOL)bridgelessEnabled
{
return YES;
if ([_delegate respondsToSelector:@selector(bridgelessEnabled)]) {
return _delegate.bridgelessEnabled;
}
return [self newArchEnabled];
}
#pragma mark - RCTTurboModuleManagerDelegate
@@ -230,9 +250,9 @@ using namespace facebook::react;
RCTRootViewFactoryConfiguration *configuration =
[[RCTRootViewFactoryConfiguration alloc] initWithBundleURLBlock:bundleUrlBlock
newArchEnabled:YES
turboModuleEnabled:YES
bridgelessEnabled:YES];
newArchEnabled:self.fabricEnabled
turboModuleEnabled:self.turboModuleEnabled
bridgelessEnabled:self.bridgelessEnabled];
configuration.createRootViewWithBridge = ^UIView *(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps) {
return [weakSelf.delegate createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
@@ -314,7 +334,9 @@ using namespace facebook::react;
dispatch_once(&setupFeatureFlagsToken, ^{
switch (releaseLevel) {
case Stable:
ReactNativeFeatureFlags::override(std::make_unique<ReactNativeFeatureFlagsOverridesOSSStable>());
if ([self bridgelessEnabled]) {
ReactNativeFeatureFlags::override(std::make_unique<ReactNativeFeatureFlagsOverridesOSSStable>());
}
break;
case Canary:
ReactNativeFeatureFlags::override(std::make_unique<ReactNativeFeatureFlagsOverridesOSSCanary>());
@@ -73,9 +73,9 @@
{
if (self = [super init]) {
_bundleURLBlock = bundleURLBlock;
_fabricEnabled = YES;
_turboModuleEnabled = YES;
_bridgelessEnabled = YES;
_fabricEnabled = newArchEnabled;
_turboModuleEnabled = turboModuleEnabled;
_bridgelessEnabled = bridgelessEnabled;
}
return self;
}
@@ -135,12 +135,17 @@
- (void)initializeReactHostWithLaunchOptions:(NSDictionary *)launchOptions
{
// Enable TurboModule interop by default in Bridgeless mode
RCTEnableTurboModuleInterop(YES);
RCTEnableTurboModuleInteropBridgeProxy(YES);
if (_configuration.bridgelessEnabled) {
// Enable TurboModule interop by default in Bridgeless mode
RCTEnableTurboModuleInterop(YES);
RCTEnableTurboModuleInteropBridgeProxy(YES);
[self createReactHostIfNeeded:launchOptions];
return;
[self createReactHostIfNeeded:launchOptions];
return;
}
[self createBridgeIfNeeded:launchOptions];
[self createBridgeAdapterIfNeeded];
}
- (UIView *)viewWithModuleName:(NSString *)moduleName
@@ -149,17 +154,29 @@
{
[self initializeReactHostWithLaunchOptions:launchOptions];
RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName
initialProperties:initProps ? initProps : @{}];
if (_configuration.bridgelessEnabled) {
RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName initialProperties:initProps];
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView =
[[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView =
[[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
if (_configuration.customizeRootView != nil) {
_configuration.customizeRootView(surfaceHostingProxyRootView);
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
if (_configuration.customizeRootView != nil) {
_configuration.customizeRootView(surfaceHostingProxyRootView);
}
return surfaceHostingProxyRootView;
}
return surfaceHostingProxyRootView;
UIView *rootView;
if (_configuration.createRootViewWithBridge != nil) {
rootView = _configuration.createRootViewWithBridge(self.bridge, moduleName, initProps);
} else {
rootView = [self createRootViewWithBridge:self.bridge moduleName:moduleName initProps:initProps];
}
if (_configuration.customizeRootView != nil) {
_configuration.customizeRootView(rootView);
}
return rootView;
}
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions
@@ -171,7 +188,8 @@
moduleName:(NSString *)moduleName
initProps:(NSDictionary *)initProps
{
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, YES);
BOOL enableFabric = _configuration.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
rootView.backgroundColor = [UIColor systemBackgroundColor];
return rootView;
}
@@ -180,15 +198,19 @@
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
_runtimeScheduler = std::make_shared<facebook::react::RuntimeScheduler>(RCTRuntimeExecutorFromBridge(bridge));
std::shared_ptr<facebook::react::CallInvoker> callInvoker =
std::make_shared<facebook::react::RuntimeSchedulerCallInvoker>(_runtimeScheduler);
RCTTurboModuleManager *turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:_turboModuleManagerDelegate
jsInvoker:callInvoker];
_contextContainer->erase("RuntimeScheduler");
_contextContainer->insert("RuntimeScheduler", _runtimeScheduler);
return RCTAppSetupDefaultJsExecutorFactory(bridge, turboModuleManager, _runtimeScheduler);
if (RCTIsNewArchEnabled()) {
std::shared_ptr<facebook::react::CallInvoker> callInvoker =
std::make_shared<facebook::react::RuntimeSchedulerCallInvoker>(_runtimeScheduler);
RCTTurboModuleManager *turboModuleManager =
[[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:_turboModuleManagerDelegate
jsInvoker:callInvoker];
_contextContainer->erase("RuntimeScheduler");
_contextContainer->insert("RuntimeScheduler", _runtimeScheduler);
return RCTAppSetupDefaultJsExecutorFactory(bridge, turboModuleManager, _runtimeScheduler);
} else {
return RCTAppSetupJsExecutorFactoryForOldArch(bridge, _runtimeScheduler);
}
}
- (void)createBridgeIfNeeded:(NSDictionary *)launchOptions
@@ -206,7 +228,7 @@
- (void)createBridgeAdapterIfNeeded
{
if (self.bridgeAdapter != nullptr) {
if (!self->_configuration.fabricEnabled || self.bridgeAdapter) {
return;
}
+3 -3
View File
@@ -120,18 +120,18 @@ class AppStateImpl {
}
switch (type) {
case 'change':
// $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const changeHandler: AppStateStatus => void = handler;
return emitter.addListener('appStateDidChange', appStateData => {
changeHandler(appStateData.app_state);
});
case 'memoryWarning':
// $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const memoryWarningHandler: () => void = handler;
return emitter.addListener('memoryWarning', memoryWarningHandler);
case 'blur':
case 'focus':
// $FlowFixMe[invalid-tuple-arity] Flow cannot refine handler based on the event type
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const focusOrBlurHandler: () => void = handler;
return emitter.addListener('appStateFocusChange', hasFocus => {
if (type === 'blur' && !hasFocus) {
@@ -162,8 +162,6 @@ class MessageQueue {
getCallableModule(name: string): {...} | null {
const getValue = this._lazyCallableModules[name];
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
return getValue ? getValue() : null;
}
@@ -468,8 +466,6 @@ class MessageQueue {
const profileName = debug
? '<callback for ' + module + '.' + method + '>'
: cbID;
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (callback && this.__spy) {
this.__spy({type: TO_JS, module: null, method: profileName, args});
}
@@ -396,7 +396,7 @@ const AccessibilityInfo = {
*/
addEventListener<K: $Keys<AccessibilityEventDefinitions>>(
eventName: K,
// $FlowFixMe[incompatible-type] - Flow bug with unions and generics (T128099423)
// $FlowIssue[incompatible-type] - Flow bug with unions and generics (T128099423)
handler: (...AccessibilityEventDefinitions[K]) => void,
): EventSubscription {
const deviceEventName = EventNames.get(eventName);
@@ -116,12 +116,6 @@ type PressableBaseProps = $ReadOnly<{
*/
onPressOut?: ?(event: GestureResponderEvent) => mixed,
/**
* Whether to prevent any other native components from becoming responder
* while this pressable is responder.
*/
blockNativeResponder?: ?boolean,
/**
* Either view styles or a function that receives a boolean reflecting whether
* the component is currently pressed and returns view styles.
@@ -189,7 +183,6 @@ function Pressable({
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
blockNativeResponder,
cancelable,
children,
delayHoverIn,
@@ -243,7 +236,7 @@ function Pressable({
};
const accessibilityLiveRegion =
ariaLive === 'off' ? 'none' : (ariaLive ?? props.accessibilityLiveRegion);
ariaLive === 'off' ? 'none' : ariaLive ?? props.accessibilityLiveRegion;
const accessibilityLabel = ariaLabel ?? props.accessibilityLabel;
const restPropsWithDefaults: React.ElementConfig<typeof View> = {
@@ -301,12 +294,10 @@ function Pressable({
onPressOut(event);
}
},
blockNativeResponder,
}),
[
android_disableSound,
android_rippleConfig,
blockNativeResponder,
cancelable,
delayHoverIn,
delayHoverOut,
@@ -235,8 +235,8 @@ class StatusBar extends React.Component<StatusBarProps> {
static _defaultProps: any = createStackEntry({
backgroundColor:
Platform.OS === 'android'
? (NativeStatusBarManagerAndroid.getConstants()
.DEFAULT_BACKGROUND_COLOR ?? 'black')
? NativeStatusBarManagerAndroid.getConstants()
.DEFAULT_BACKGROUND_COLOR ?? 'black'
: 'black',
barStyle: 'default',
translucent: false,
@@ -215,7 +215,7 @@ const Switch: component(
native.value != null && native.value !== jsValue;
if (
shouldUpdateNativeSwitch &&
// $FlowFixMe[method-unbinding]
// $FlowIssue[method-unbinding]
nativeSwitchRef.current?.setNativeProps != null
) {
if (Platform.OS === 'android') {
@@ -618,9 +618,6 @@ function InternalTextInput(props: TextInputProps): React.Node {
// so omitting onBlur and onFocus pressability handlers here.
const {onBlur, onFocus, ...eventHandlers} = usePressability(config);
const _accessibilityLabel =
props?.['aria-label'] ?? props?.accessibilityLabel;
let _accessibilityState;
if (
accessibilityState != null ||
@@ -684,7 +681,6 @@ function InternalTextInput(props: TextInputProps): React.Node {
{...otherProps}
{...eventHandlers}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
accessible={accessible}
submitBehavior={submitBehavior}
@@ -748,9 +744,8 @@ function InternalTextInput(props: TextInputProps): React.Node {
{...otherProps}
{...colorProps}
{...eventHandlers}
accessibilityLabel={_accessibilityLabel}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityState={_accessibilityState}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessible={accessible}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
autoCapitalize={autoCapitalize}
@@ -920,8 +915,8 @@ const TextInput: component(
Platform.OS === 'android'
? // $FlowFixMe[invalid-computed-prop]
// $FlowFixMe[prop-missing]
(autoCompleteWebToAutoCompleteAndroidMap[autoComplete] ??
autoComplete)
autoCompleteWebToAutoCompleteAndroidMap[autoComplete] ??
autoComplete
: undefined
}
textContentType={
@@ -10,404 +10,213 @@
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import type {TextInputInstance} from '../TextInput.flow';
import type {HostInstance} from 'react-native';
import ensureInstance from '../../../../src/private/__tests__/utilities/ensureInstance';
import * as Fantom from '@react-native/fantom';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {createRef, useEffect, useLayoutEffect, useRef} from 'react';
import {TextInput} from 'react-native';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
describe('<TextInput>', () => {
describe('props', () => {
describe('selection', () => {
it('the selection is passed to component view by command', () => {
const root = Fantom.createRoot();
describe('focus view command', () => {
it('creates view before dispatching view command from ref function', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<TextInput nativeID="text-input" selection={{start: 0, end: 4}}>
hello World!
</TextInput>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,0,4]"}',
]);
});
Fantom.runTask(() => {
root.render(
<TextInput
nativeID="text-input"
ref={node => {
if (node) {
node.focus();
}
}}
/>,
);
});
describe('onChange', () => {
it('is called when the change native event is dispatched', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<TextInputInstance>();
const onChange = jest.fn();
Fantom.runTask(() => {
root.render(
<TextInput
onChange={event => {
onChange(event.nativeEvent);
}}
ref={nodeRef}
/>,
);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'change', {
text: 'Hello World',
});
});
Fantom.runWorkLoop();
expect(onChange).toHaveBeenCalledTimes(1);
const [entry] = onChange.mock.lastCall;
expect(entry.text).toEqual('Hello World');
});
});
describe('onChangeText', () => {
it('is called when the change native event is dispatched', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<TextInputInstance>();
const onChangeText = jest.fn();
Fantom.runTask(() => {
root.render(<TextInput onChangeText={onChangeText} ref={nodeRef} />);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'change', {
text: 'Hello World',
});
});
Fantom.runWorkLoop();
expect(onChangeText).toHaveBeenCalledTimes(1);
const [entry] = onChangeText.mock.lastCall;
expect(entry).toEqual('Hello World');
});
});
describe('onFocus', () => {
it('is called when the focus native event is dispatched', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<TextInputInstance>();
let focusEvent = jest.fn();
Fantom.runTask(() => {
root.render(<TextInput onFocus={focusEvent} ref={nodeRef} />);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
expect(focusEvent).toHaveBeenCalledTimes(0);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'focus');
});
// The tasks have not run.
expect(focusEvent).toHaveBeenCalledTimes(0);
Fantom.runWorkLoop();
expect(focusEvent).toHaveBeenCalledTimes(1);
});
});
describe('onBlur', () => {
it('is called when the blur native event is dispatched', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<TextInputInstance>();
let blurEvent = jest.fn();
Fantom.runTask(() => {
root.render(<TextInput onBlur={blurEvent} ref={nodeRef} />);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
expect(blurEvent).toHaveBeenCalledTimes(0);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'blur');
});
// The tasks have not run.
expect(blurEvent).toHaveBeenCalledTimes(0);
Fantom.runWorkLoop();
expect(blurEvent).toHaveBeenCalledTimes(1);
});
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
describe('ref', () => {
it('is an element node', () => {
const ref = createRef<TextInputInstance>();
it('creates view before dispatching view command from useLayoutEffect', () => {
const root = Fantom.createRoot();
const root = Fantom.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
Fantom.runTask(() => {
root.render(<TextInput ref={ref} />);
useLayoutEffect(() => {
textInputRef.current?.focus();
});
expect(ref.current).toBeInstanceOf(ReactNativeElement);
return <TextInput ref={textInputRef} nativeID="text-input" />;
}
Fantom.runTask(() => {
root.render(<Component />);
});
it('provides additional methods: clear, isFocused, getNativeRef, setSelection', () => {
const ref = createRef<TextInputInstance>();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
const root = Fantom.createRoot();
it('creates view before dispatching view command from useEffect', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<TextInput ref={ref} />);
function Component() {
const textInputRef = useRef<null | React.ElementRef<typeof TextInput>>(
null,
);
useEffect(() => {
textInputRef.current?.focus();
});
const instance = nullthrows(ref.current);
expect(instance.clear).toBeInstanceOf(Function);
expect(instance.isFocused).toBeInstanceOf(Function);
expect(instance.getNativeRef).toBeInstanceOf(Function);
return <TextInput ref={textInputRef} nativeID="text-input" />;
}
Fantom.runTask(() => {
root.render(<Component />);
});
describe('focus()', () => {
it('dispatches the focus command', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
});
describe('focus and blur event', () => {
it('sends focus and blur events', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
let focusEvent = jest.fn();
let blurEvent = jest.fn();
Fantom.runTask(() => {
root.render(
<TextInput onFocus={focusEvent} onBlur={blurEvent} ref={nodeRef} />,
);
});
Fantom.runTask(() => {
root.render(<TextInput nativeID="text-input" ref={ref} />);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
root.takeMountingManagerLogs();
expect(focusEvent).toHaveBeenCalledTimes(0);
expect(blurEvent).toHaveBeenCalledTimes(0);
const instance = nullthrows(ref.current);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'focus');
});
Fantom.runTask(() => {
instance.focus();
});
// The tasks have not run.
expect(focusEvent).toHaveBeenCalledTimes(0);
expect(blurEvent).toHaveBeenCalledTimes(0);
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
Fantom.runWorkLoop();
it('creates view before dispatching view command from ref function', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<TextInput
nativeID="text-input"
ref={node => {
if (node) {
node.focus();
}
}}
/>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
expect(focusEvent).toHaveBeenCalledTimes(1);
expect(blurEvent).toHaveBeenCalledTimes(0);
it('creates view before dispatching view command from useLayoutEffect', () => {
const root = Fantom.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<
typeof TextInput,
>>(null);
useLayoutEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} nativeID="text-input" />;
}
Fantom.runTask(() => {
root.render(<Component />);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'blur');
});
it('creates view before dispatching view command from useEffect', () => {
const root = Fantom.createRoot();
function Component() {
const textInputRef = useRef<null | React.ElementRef<
typeof TextInput,
>>(null);
useEffect(() => {
textInputRef.current?.focus();
});
return <TextInput ref={textInputRef} nativeID="text-input" />;
}
Fantom.runWorkLoop();
expect(focusEvent).toHaveBeenCalledTimes(1);
expect(blurEvent).toHaveBeenCalledTimes(1);
});
});
describe('onChange', () => {
it('delivers onChange event', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onChange = jest.fn();
Fantom.runTask(() => {
root.render(
<TextInput
onChange={event => {
onChange(event.nativeEvent);
}}
ref={nodeRef}
/>,
);
});
Fantom.runTask(() => {
root.render(<Component />);
});
const element = ensureInstance(nodeRef.current, ReactNativeElement);
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'change', {
text: 'Hello World',
});
});
describe('blur()', () => {
it('does NOT dispatch any commands if the input is NOT focused', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
Fantom.runTask(() => {
root.render(<TextInput nativeID="text-input" ref={ref} />);
});
root.takeMountingManagerLogs();
const instance = nullthrows(ref.current);
Fantom.runTask(() => {
instance.blur();
});
expect(root.takeMountingManagerLogs()).toEqual([]);
});
it('does dispatches the blur command if the input is focused', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
Fantom.runTask(() => {
root.render(<TextInput nativeID="text-input" ref={ref} />);
});
const instance = nullthrows(ref.current);
Fantom.runWorkLoop();
Fantom.runTask(() => {
instance.focus();
});
expect(onChange).toHaveBeenCalledTimes(1);
const [entry] = onChange.mock.lastCall;
expect(entry.text).toEqual('Hello World');
});
});
root.takeMountingManagerLogs();
describe('onChangeText', () => {
it('delivers onChangeText event', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onChangeText = jest.fn();
Fantom.runTask(() => {
instance.blur();
});
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "blur"}',
]);
});
Fantom.runTask(() => {
root.render(<TextInput onChangeText={onChangeText} ref={nodeRef} />);
});
describe('clear()', () => {
it('dispatches the clear command', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
Fantom.runTask(() => {
root.render(
<TextInput nativeID="text-input" ref={ref} value="Some input" />,
);
});
root.takeMountingManagerLogs();
const instance = nullthrows(ref.current);
const element = ensureInstance(nodeRef.current, ReactNativeElement);
Fantom.runTask(() => {
instance.clear();
});
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,"",0,0]"}',
]);
Fantom.runOnUIThread(() => {
Fantom.enqueueNativeEvent(element, 'change', {
text: 'Hello World',
});
});
describe('isFocused()', () => {
it('returns true if the input is focused', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
Fantom.runTask(() => {
root.render(<TextInput nativeID="text-input" ref={ref} />);
});
const instance = nullthrows(ref.current);
expect(instance.isFocused()).toBe(false);
Fantom.runWorkLoop();
Fantom.runTask(() => {
instance.focus();
});
expect(onChangeText).toHaveBeenCalledTimes(1);
const [entry] = onChangeText.mock.lastCall;
expect(entry).toEqual('Hello World');
});
});
expect(instance.isFocused()).toBe(true);
describe('props.selection', () => {
it('the selection is passed to component view by command', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
instance.blur();
});
expect(instance.isFocused()).toBe(false);
});
Fantom.runTask(() => {
root.render(
<TextInput nativeID="text-input" selection={{start: 0, end: 4}}>
hello World!
</TextInput>,
);
});
describe('setSelection', () => {
it('dispatches the setTextAndSelection command', () => {
const root = Fantom.createRoot();
const ref = createRef<TextInputInstance>();
Fantom.runTask(() => {
root.render(
<TextInput nativeID="text-input" ref={ref} value="Some input" />,
);
});
root.takeMountingManagerLogs();
const instance = nullthrows(ref.current);
Fantom.runTask(() => {
instance.setSelection(2, 5);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,2,5]"}',
]);
});
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "AndroidTextInput", nativeID: "text-input"}',
'Insert {type: "AndroidTextInput", parentNativeID: (root), index: 0, nativeID: "text-input"}',
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,null,0,4]"}',
]);
});
});
@@ -432,7 +432,6 @@ jest.unmock('../TextInput');
expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityLabel="label"
accessibilityState={
Object {
"busy": true,
@@ -138,7 +138,7 @@ class TouchableBounce extends React.Component<
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
: this.props['aria-live'] ?? this.props.accessibilityLiveRegion;
const _accessibilityState = {
busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy,
checked:
@@ -328,7 +328,7 @@ class TouchableHighlightImpl extends React.Component<
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
: this.props['aria-live'] ?? this.props.accessibilityLiveRegion;
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
@@ -332,7 +332,7 @@ class TouchableNativeFeedback extends React.Component<
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
: this.props['aria-live'] ?? this.props.accessibilityLiveRegion;
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
@@ -294,7 +294,7 @@ class TouchableOpacity extends React.Component<
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
: this.props['aria-live'] ?? this.props.accessibilityLiveRegion;
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
@@ -189,7 +189,7 @@ export default function TouchableWithoutFeedback(
disabled:
disabled !== null
? disabled
: (ariaDisabled ?? accessibilityState?.disabled),
: ariaDisabled ?? accessibilityState?.disabled,
hitSlop: hitSlop,
delayLongPress: delayLongPress,
delayPressIn: delayPressIn,
@@ -272,7 +272,7 @@ export default function TouchableWithoutFeedback(
? 'no-hide-descendants'
: props.importantForAccessibility,
accessibilityLiveRegion:
ariaLive === 'off' ? 'none' : (ariaLive ?? props.accessibilityLiveRegion),
ariaLive === 'off' ? 'none' : ariaLive ?? props.accessibilityLiveRegion,
nativeID: props.id ?? props.nativeID,
};
+169 -87
View File
@@ -10,6 +10,7 @@
import type {ViewProps} from './ViewPropTypes';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import TextAncestorContext from '../../Text/TextAncestorContext';
import ViewNativeComponent from './ViewNativeComponent';
import * as React from 'react';
@@ -22,103 +23,188 @@ import {use} from 'react';
*
* @see https://reactnative.dev/docs/view
*/
component View(
export default component View(
ref?: React.RefSetter<React.ElementRef<typeof ViewNativeComponent>>,
...props: ViewProps
) {
const hasTextAncestor = use(TextAncestorContext);
const {
accessibilityState,
accessibilityValue,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-hidden': ariaHidden,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-live': ariaLive,
'aria-selected': ariaSelected,
'aria-valuemax': ariaValueMax,
'aria-valuemin': ariaValueMin,
'aria-valuenow': ariaValueNow,
'aria-valuetext': ariaValueText,
id,
tabIndex,
...otherProps
} = props;
let actualView;
if (ReactNativeFeatureFlags.reduceDefaultPropsInView()) {
const {
accessibilityState,
accessibilityValue,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-hidden': ariaHidden,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-live': ariaLive,
'aria-selected': ariaSelected,
'aria-valuemax': ariaValueMax,
'aria-valuemin': ariaValueMin,
'aria-valuenow': ariaValueNow,
'aria-valuetext': ariaValueText,
id,
tabIndex,
...otherProps
} = props;
// Since we destructured props, we can now treat it as mutable
const processedProps = otherProps as {...ViewProps};
// Since we destructured props, we can now treat it as mutable
const processedProps = otherProps as {...ViewProps};
const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g);
if (parsedAriaLabelledBy !== undefined) {
processedProps.accessibilityLabelledBy = parsedAriaLabelledBy;
}
if (ariaLabel !== undefined) {
processedProps.accessibilityLabel = ariaLabel;
}
if (ariaLive !== undefined) {
processedProps.accessibilityLiveRegion =
ariaLive === 'off' ? 'none' : ariaLive;
}
if (ariaHidden !== undefined) {
processedProps.accessibilityElementsHidden = ariaHidden;
if (ariaHidden === true) {
processedProps.importantForAccessibility = 'no-hide-descendants';
const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g);
if (parsedAriaLabelledBy !== undefined) {
processedProps.accessibilityLabelledBy = parsedAriaLabelledBy;
}
}
if (id !== undefined) {
processedProps.nativeID = id;
}
if (ariaLabel !== undefined) {
processedProps.accessibilityLabel = ariaLabel;
}
if (tabIndex !== undefined) {
processedProps.focusable = !tabIndex;
}
if (ariaLive !== undefined) {
processedProps.accessibilityLiveRegion =
ariaLive === 'off' ? 'none' : ariaLive;
}
if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
processedProps.accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}
if (ariaHidden !== undefined) {
processedProps.accessibilityElementsHidden = ariaHidden;
if (ariaHidden === true) {
processedProps.importantForAccessibility = 'no-hide-descendants';
}
}
if (
accessibilityValue != null ||
ariaValueMax != null ||
ariaValueMin != null ||
ariaValueNow != null ||
ariaValueText != null
) {
processedProps.accessibilityValue = {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
};
}
if (id !== undefined) {
processedProps.nativeID = id;
}
const actualView =
ref == null ? (
<ViewNativeComponent {...processedProps} />
) : (
<ViewNativeComponent {...processedProps} ref={ref} />
if (tabIndex !== undefined) {
processedProps.focusable = !tabIndex;
}
if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
processedProps.accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}
if (
accessibilityValue != null ||
ariaValueMax != null ||
ariaValueMin != null ||
ariaValueNow != null ||
ariaValueText != null
) {
processedProps.accessibilityValue = {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
};
}
actualView =
ref == null ? (
<ViewNativeComponent {...processedProps} />
) : (
<ViewNativeComponent {...processedProps} ref={ref} />
);
} else {
const {
accessibilityElementsHidden,
accessibilityLabel,
accessibilityLabelledBy,
accessibilityLiveRegion,
accessibilityState,
accessibilityValue,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-hidden': ariaHidden,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-live': ariaLive,
'aria-selected': ariaSelected,
'aria-valuemax': ariaValueMax,
'aria-valuemin': ariaValueMin,
'aria-valuenow': ariaValueNow,
'aria-valuetext': ariaValueText,
focusable,
id,
importantForAccessibility,
nativeID,
tabIndex,
...otherProps
} = props;
const _accessibilityLabelledBy =
ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy;
const _accessibilityState =
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
? {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
}
: undefined;
const _accessibilityValue =
accessibilityValue != null ||
ariaValueMax != null ||
ariaValueMin != null ||
ariaValueNow != null ||
ariaValueText != null
? {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
}
: undefined;
actualView = (
<ViewNativeComponent
{...otherProps}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? accessibilityLiveRegion
}
accessibilityLabel={ariaLabel ?? accessibilityLabel}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
accessibilityState={_accessibilityState}
accessibilityElementsHidden={ariaHidden ?? accessibilityElementsHidden}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityValue={_accessibilityValue}
importantForAccessibility={
ariaHidden === true
? 'no-hide-descendants'
: importantForAccessibility
}
nativeID={id ?? nativeID}
ref={ref}
/>
);
}
if (hasTextAncestor) {
return (
@@ -127,7 +213,3 @@ component View(
}
return actualView;
}
View.displayName = 'View';
export default View;
@@ -15,8 +15,7 @@ import * as React from 'react';
import {View} from 'react-native';
let root;
let testViews: React.MixedElement;
let thousandViews: React.MixedElement;
function createViewsWithLargeAmountOfPropsAndStyles(count: number): React.Node {
let views: React.Node = null;
for (let i = 0; i < count; i++) {
@@ -75,16 +74,15 @@ function createViewsWithLargeAmountOfPropsAndStyles(count: number): React.Node {
Fantom.unstable_benchmark
.suite('View')
.test.each(
[100, 1000],
n => `render ${n.toString()} uncollapsable views`,
.test(
'render 100 uncollapsable views',
() => {
Fantom.runTask(() => root.render(testViews));
Fantom.runTask(() => root.render(thousandViews));
},
n => ({
{
beforeAll: () => {
let views: React.Node = null;
for (let i = 0; i < n; i++) {
for (let i = 0; i < 100; i++) {
views = (
<View
collapsable={false}
@@ -96,7 +94,7 @@ Fantom.unstable_benchmark
);
}
// $FlowExpectedError[incompatible-type]
testViews = views;
thousandViews = views;
},
beforeEach: () => {
root = Fantom.createRoot();
@@ -104,18 +102,47 @@ Fantom.unstable_benchmark
afterEach: () => {
root.destroy();
},
}),
)
.test.each(
[100, 1000, 1500],
n => `render ${n.toString()} views with large amount of props and styles`,
() => {
Fantom.runTask(() => root.render(testViews));
},
n => ({
)
.test(
'render 1000 uncollapsable views',
() => {
Fantom.runTask(() => root.render(thousandViews));
},
{
beforeAll: () => {
let views: React.Node = null;
for (let i = 0; i < 1000; i++) {
views = (
<View
collapsable={false}
id={String(i)}
nativeID={String(i)}
style={{width: i + 1, height: i + 1}}>
{views}
</View>
);
}
// $FlowExpectedError[incompatible-type]
thousandViews = views;
},
beforeEach: () => {
root = Fantom.createRoot();
},
afterEach: () => {
root.destroy();
},
},
)
.test(
'render 100 views with large amount of props and styles',
() => {
Fantom.runTask(() => root.render(thousandViews));
},
{
beforeAll: () => {
// $FlowExpectedError[incompatible-type]
testViews = createViewsWithLargeAmountOfPropsAndStyles(n);
thousandViews = createViewsWithLargeAmountOfPropsAndStyles(100);
},
beforeEach: () => {
root = Fantom.createRoot();
@@ -123,5 +150,41 @@ Fantom.unstable_benchmark
afterEach: () => {
root.destroy();
},
}),
},
)
.test(
'render 1000 views with large amount of props and styles',
() => {
Fantom.runTask(() => root.render(thousandViews));
},
{
beforeAll: () => {
// $FlowExpectedError[incompatible-type]
thousandViews = createViewsWithLargeAmountOfPropsAndStyles(1000);
},
beforeEach: () => {
root = Fantom.createRoot();
},
afterEach: () => {
root.destroy();
},
},
)
.test(
'render 1500 views with large amount of props and styles',
() => {
Fantom.runTask(() => root.render(thousandViews));
},
{
beforeAll: () => {
// $FlowExpectedError[incompatible-type]
thousandViews = createViewsWithLargeAmountOfPropsAndStyles(1500);
},
beforeEach: () => {
root = Fantom.createRoot();
},
afterEach: () => {
root.destroy();
},
},
);
@@ -20,217 +20,209 @@ import {View} from 'react-native';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
describe('<View>', () => {
describe('props', () => {
describe('style', () => {
describe('width and height style', () => {
it('handles correct percentage-based dimensions', () => {
const root = Fantom.createRoot();
describe('width and height style', () => {
it('handles correct percentage-based dimensions', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100}}>
<View
style={{width: '20%', height: '50%'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:20,height:50}"
height="50.000000%"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="20.000000%"
/>,
);
});
it('handles numeric values passed in as strings', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: '5', height: '10'}} collapsable={false} />,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="5.000000"
/>,
);
});
it('handles invalid values, falling back to default', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100}}>
<View
// 5pt is a valid CSS value but RN can't parse it.
style={{width: '5pt', height: 'error 50%'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:100,height:0}"
height="undefined"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="undefined"
/>,
);
});
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100}}>
<View style={{width: '20%', height: '50%'}} collapsable={false} />
</View>,
);
});
describe('margin style', () => {
it('handles correct percentage-based values', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 200}}>
<View
style={{width: 5, height: 10, margin: '50%'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:50,y:50,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
margin="50.000000%"
width="5.000000"
/>,
);
});
it('handles numeric values passed in as strings', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 200}}>
<View
style={{width: 5, height: 10, margin: '5'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:5,y:5,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
margin="5.000000"
width="5.000000"
/>,
);
});
});
describe('transform style', () => {
it('causes view to be unflattened', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View style={{transform: [{translateX: 10}]}} />);
});
expect(
root.getRenderedOutput({props: ['transform']}).toJSX(),
).toEqual(<rn-view transform='[{"translateX": 10.000000}]' />);
});
[
[undefined, {x: -5, y: 0, width: 20, height: 10}],
['50% 50%', {x: -5, y: 0, width: 20, height: 10}],
['top left', {x: 0, y: 0, width: 20, height: 10}],
['right bottom', {x: -10, y: 0, width: 20, height: 10}],
].forEach(([transformOrigin, expectedBounds]) => {
it(`applies transformOrigin correctly for ${String(transformOrigin)}`, () => {
const root = Fantom.createRoot();
const viewRef = createRef<HostInstance>();
Fantom.runTask(() => {
root.render(
<View
ref={viewRef}
style={{
width: 10,
height: 10,
transform: [{scaleX: 2}],
transformOrigin,
}}
/>,
);
});
const viewElement = ensureInstance(
viewRef.current,
ReactNativeElement,
);
const viewBounds = viewElement.getBoundingClientRect();
expect(viewBounds.x).toBe(expectedBounds.x);
expect(viewBounds.y).toBe(expectedBounds.y);
expect(viewBounds.width).toBe(expectedBounds.width);
expect(viewBounds.height).toBe(expectedBounds.height);
});
});
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:20,height:50}"
height="50.000000%"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="20.000000%"
/>,
);
});
it('handles numeric values passed in as strings', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: '5', height: '10'}} collapsable={false} />,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="5.000000"
/>,
);
});
it('handles invalid values, falling back to default', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 100}}>
<View
// 5pt is a valid CSS value but RN can't parse it.
style={{width: '5pt', height: 'error 50%'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:0,y:0,width:100,height:0}"
height="undefined"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
width="undefined"
/>,
);
});
});
describe('margin style', () => {
it('handles correct percentage-based values', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 200}}>
<View
style={{width: 5, height: 10, margin: '50%'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:50,y:50,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
margin="50.000000%"
width="5.000000"
/>,
);
});
it('handles numeric values passed in as strings', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View style={{width: 100, height: 200}}>
<View
style={{width: 5, height: 10, margin: '5'}}
collapsable={false}
/>
</View>,
);
});
expect(
root.getRenderedOutput({includeLayoutMetrics: true}).toJSX(),
).toEqual(
<rn-view
layoutMetrics-frame="{x:5,y:5,width:5,height:10}"
height="10.000000"
layoutMetrics-borderWidth="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-contentInsets="{top:0,right:0,bottom:0,left:0}"
layoutMetrics-displayType="Flex"
layoutMetrics-layoutDirection="LeftToRight"
layoutMetrics-overflowInset="{top:0,right:-0,bottom:-0,left:0}"
layoutMetrics-pointScaleFactor="3"
margin="5.000000"
width="5.000000"
/>,
);
});
});
describe('transform style', () => {
it('causes view to be unflattened', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View style={{transform: [{translateX: 10}]}} />);
});
expect(root.getRenderedOutput({props: ['transform']}).toJSX()).toEqual(
<rn-view transform='[{"translateX": 10.000000}]' />,
);
});
[
[undefined, {x: -5, y: 0, width: 20, height: 10}],
['50% 50%', {x: -5, y: 0, width: 20, height: 10}],
['top left', {x: 0, y: 0, width: 20, height: 10}],
['right bottom', {x: -10, y: 0, width: 20, height: 10}],
].forEach(([transformOrigin, expectedBounds]) => {
it(`applies transformOrigin correctly for ${String(transformOrigin)}`, () => {
const root = Fantom.createRoot();
const viewRef = createRef<HostInstance>();
Fantom.runTask(() => {
root.render(
<View
ref={viewRef}
style={{
width: 10,
height: 10,
transform: [{scaleX: 2}],
transformOrigin,
}}
/>,
);
});
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
const viewBounds = viewElement.getBoundingClientRect();
expect(viewBounds.x).toBe(expectedBounds.x);
expect(viewBounds.y).toBe(expectedBounds.y);
expect(viewBounds.width).toBe(expectedBounds.width);
expect(viewBounds.height).toBe(expectedBounds.height);
});
});
});
describe('props', () => {
describe('pointerEvents', () => {
it('auto does not propagate to the mounting layer, it is the default', () => {
const root = Fantom.createRoot();
@@ -293,7 +285,6 @@ describe('<View>', () => {
).toEqual(<rn-view pointerEvents="none" />);
});
});
describe('accessibility', () => {
describe('accessibilityActions', () => {
it('is propagated to the mounting layer', () => {
@@ -491,31 +482,4 @@ describe('<View>', () => {
});
});
});
describe('ref', () => {
it('is an element node', () => {
const elementRef = createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View ref={elementRef} />);
});
expect(elementRef.current).toBeInstanceOf(ReactNativeElement);
});
it('uses the "RN:View" tag name', () => {
const elementRef = createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View ref={elementRef} />);
});
const element = ensureInstance(elementRef.current, ReactNativeElement);
expect(element.tagName).toBe('RN:View');
});
});
});
@@ -34,7 +34,7 @@ describe('JSTimers', () => {
});
afterEach(() => {
// $FlowFixMe[prop-missing]
// $FlowIssue[prop-missing]
console.warn.mockRestore();
});
@@ -477,7 +477,7 @@ function runExceptionsManagerTests() {
expect(nativeReportException).not.toBeCalled();
expect(logBoxAddConsoleLog).toBeCalledTimes(1);
expect(logBoxAddConsoleLog.mock.calls[0][0]).toBe('error');
// $FlowFixMe[incompatible-call]
// $FlowIgnore[incompatible-call]
expect(logBoxAddConsoleLog.mock.calls[0][1]).toBe(...args);
} else {
expect(logBoxAddException).not.toBeCalled();
@@ -554,7 +554,7 @@ function runExceptionsManagerTests() {
const object = {
toString: () => 'Warning: Some error may have happened',
};
// $FlowFixMe[prop-missing]
// $FlowIgnore[prop-missing]
object.cycle = object;
const args = [object];
@@ -27,12 +27,12 @@ function _setDevelopmentModeForTests(dev: mixed) {
beforeAll(() => {
originalDev = global.__DEV__;
// $FlowFixMe[cannot-write]
// $FlowIgnore[cannot-write]
global.__DEV__ = dev;
});
afterAll(() => {
// $FlowFixMe[cannot-write]
// $FlowIgnore[cannot-write]
global.__DEV__ = originalDev;
});
}

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