mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e454af912 |
+5
-1
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+10
-12
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]});
|
||||
}
|
||||
|
||||
|
||||
@@ -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]});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
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,
|
||||
) => {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
-37
@@ -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
|
||||
|
||||
+5
-1
@@ -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(
|
||||
|
||||
+28
-13
@@ -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",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-1
@@ -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() =
|
||||
|
||||
+64
-2
@@ -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
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+1
-1
@@ -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(() =>
|
||||
|
||||
Vendored
+2
-2
@@ -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');
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
-133
@@ -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);
|
||||
|
||||
+32
-32
@@ -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));
|
||||
|
||||
+20
-20
@@ -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);
|
||||
|
||||
+18
-18
@@ -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
@@ -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});
|
||||
}
|
||||
|
||||
+1
-1
@@ -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={
|
||||
|
||||
+156
-347
@@ -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]"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
@@ -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;
|
||||
|
||||
+82
-19
@@ -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();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
+198
-234
@@ -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];
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user