mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Compare commits
115
Commits
@@ -76,10 +76,6 @@ module.system.haste.module_ref_prefix=m#
|
||||
react.runtime=automatic
|
||||
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
suppress_type=$FlowFixMeEmpty
|
||||
|
||||
ban_spread_key_props=true
|
||||
|
||||
|
||||
@@ -0,0 +1,897 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,15 @@ 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';
|
||||
@@ -103,16 +110,62 @@ 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 to discord');
|
||||
console.log('Sending current failures to Discord...');
|
||||
await notifyDiscord(discordWebHook, failures);
|
||||
} else {
|
||||
console.log('Web hook not set');
|
||||
console.log('Discord webhook not set');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ All tests passed!');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -25,6 +25,31 @@ 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);
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
@@ -41,20 +41,12 @@ async function sendMessageToDiscord(webHook, message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a formatted Discord message payload from a list of failures.
|
||||
* @param {Array<Object>} failures - List of failures to format
|
||||
* @returns {Object} - The formatted Discord message payload
|
||||
* 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 prepareFailurePayload(failures) {
|
||||
if (!failures || failures.length === 0) {
|
||||
return {
|
||||
content:
|
||||
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.',
|
||||
};
|
||||
}
|
||||
|
||||
// Sort failures by platform and then by library name
|
||||
const sortedFailures = [...failures].sort((a, b) => {
|
||||
function sortResultsByPlatformAndLibrary(jobs) {
|
||||
return [...jobs].sort((a, b) => {
|
||||
// First sort by platform
|
||||
const platformA = a.platform || 'Unknown';
|
||||
const platformB = b.platform || 'Unknown';
|
||||
@@ -68,6 +60,23 @@ function prepareFailurePayload(failures) {
|
||||
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
|
||||
* @returns {Object} - The formatted Discord message payload
|
||||
*/
|
||||
function prepareFailurePayload(failures) {
|
||||
if (!failures || failures.length === 0) {
|
||||
return {
|
||||
content:
|
||||
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.',
|
||||
};
|
||||
}
|
||||
|
||||
// Sort failures by platform and then by library name
|
||||
const sortedFailures = sortResultsByPlatformAndLibrary(failures);
|
||||
|
||||
// Format the failures into a message
|
||||
const formattedFailures = sortedFailures
|
||||
@@ -83,8 +92,45 @@ 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,3 +32,7 @@ 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,10 +27,6 @@ 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:
|
||||
|
||||
@@ -592,7 +592,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: ["24", "22", "20.19.4"]
|
||||
node-version: ["24.4.1", "22", "20.19.4"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -5,6 +5,14 @@ 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
|
||||
@@ -94,6 +102,11 @@ 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,6 +170,7 @@ 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
|
||||
|
||||
+26
-206
@@ -1,218 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## v0.81.0-rc.3
|
||||
|
||||
### Breaking
|
||||
|
||||
- Metro to ^0.83.1 ([e247be793c](https://github.com/facebook/react-native/commit/e247be793c70a374955d798d8cbbc6eba58080ec) by [@motiz88](https://github.com/motiz88))
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
## v0.81.0-rc.5
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Android specific
|
||||
- **Runtime:** Fixed `ReactHostImpl.nativeModules` always returning an empty list ([2f46a49](https://github.com/facebook/react-native/commit/2f46a49b8d8a11d5cf4342eee83c469b545c6779) by [@lukmccall](https://github.com/lukmccall))
|
||||
|
||||
## v0.81.0-rc.4 - Burned
|
||||
|
||||
## v0.81.0-rc.3
|
||||
|
||||
### Changed
|
||||
|
||||
- **Metro:** Metro to ^0.83.1 ([e247be793c](https://github.com/facebook/react-native/commit/e247be793c70a374955d798d8cbbc6eba58080ec) by [@motiz88](https://github.com/motiz88))
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Android specific
|
||||
|
||||
- **rngp:** Fix a race condition with codegen libraries missing sources ([9013a9e666](https://github.com/facebook/react-native/commit/9013a9e66629677c47e1b69703f9fc8f4cbc1c2c) by [@cortinico](https://github.com/cortinico))
|
||||
- Make accessors inside HeadlessJsTaskService open again ([7ef57163cb](https://github.com/facebook/react-native/commit/7ef57163cb016317e43e563da7ea181989f6abca) by [@cortinico](https://github.com/cortinico))
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Unknown
|
||||
|
||||
- Release 0.81.0-rc.3 ([0e6009eecf](https://github.com/facebook/react-native/commit/0e6009eecfeac71121a045f0f7e6bae94ffc11b0) by [@react-native-bot](https://github.com/react-native-bot))
|
||||
- Bump Podfile.lock ([3695258eed](https://github.com/facebook/react-native/commit/3695258eed45fed5fbff5dd6128c88275f91083c) by [@react-native-bot](https://github.com/react-native-bot))
|
||||
|
||||
#### Android Unknown
|
||||
|
||||
|
||||
|
||||
#### iOS Unknown
|
||||
|
||||
|
||||
|
||||
#### Failed to parse
|
||||
|
||||
|
||||
|
||||
- **API:** Make accessors inside HeadlessJsTaskService open again ([7ef57163cb](https://github.com/facebook/react-native/commit/7ef57163cb016317e43e563da7ea181989f6abca) by [@cortinico](https://github.com/cortinico))
|
||||
|
||||
## v0.81.0-rc.2
|
||||
|
||||
### Breaking
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added support to `react-native/babel-preset` for a `hermesParserOptions` option, that expects an object that enables overriding `hermes-parser` options. ([0508eddfe6](https://github.com/facebook/react-native/commit/0508eddfe60df60cb3bfa4074ae199bd0e492d5f) by [@yungsters](https://github.com/yungsters))
|
||||
- `NewAppScreen` no longer internally handles device safe area, use optional `safeAreaInsets` prop (aligned in 0.81 template) ([732bd12dc2](https://github.com/facebook/react-native/commit/732bd12dc21460641ef01b23f2eb722f26b060d5) by [@huntie](https://github.com/huntie))
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
- **API:** `NewAppScreen` no longer internally handles device safe area, use optional `safeAreaInsets` prop (aligned in 0.81 template) ([732bd12dc2](https://github.com/facebook/react-native/commit/732bd12dc21460641ef01b23f2eb722f26b060d5) by [@huntie](https://github.com/huntie))
|
||||
- **Babel:** Added support to `react-native/babel-preset` for a `hermesParserOptions` option, that expects an object that enables overriding `hermes-parser` options. ([0508eddfe6](https://github.com/facebook/react-native/commit/0508eddfe60df60cb3bfa4074ae199bd0e492d5f) by [@yungsters](https://github.com/yungsters))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- Fixed issue with RNDeps release/debug switch failing ([4ee2b60a1e](https://github.com/facebook/react-native/commit/4ee2b60a1eacca744d58a7ad336ca9d3714289f6) by [@chrfalch](https://github.com/chrfalch))
|
||||
- Fixed missing script for resolving prebuilt xcframework when switching between release/debug ([2e55241a90](https://github.com/facebook/react-native/commit/2e55241a901b4cd95917de68ce9078928820a208) by [@chrfalch](https://github.com/chrfalch))
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
|
||||
#### Android specific
|
||||
|
||||
|
||||
|
||||
#### iOS specific
|
||||
|
||||
|
||||
|
||||
### Unknown
|
||||
|
||||
- Release 0.81.0-rc.2 ([68ef746ec5](https://github.com/facebook/react-native/commit/68ef746ec5dd7d2874d190733e75bb8197034c5a) by [@react-native-bot](https://github.com/react-native-bot))
|
||||
- Fix E2E test script when the ci flag is not specified ([cdd7f99581](https://github.com/facebook/react-native/commit/cdd7f995813727b630ff38abc8a910a0f8f10b37) by [@cipolleschi](https://github.com/cipolleschi))
|
||||
- Fix E2E script when using CI artifacts ([d8bf94489a](https://github.com/facebook/react-native/commit/d8bf94489ab7498eba7e5f45d09dd2819fe739c3) by [@cipolleschi](https://github.com/cipolleschi))
|
||||
- Bump Podfile.lock ([10b63c15b6](https://github.com/facebook/react-native/commit/10b63c15b6faaf54d555ede879f82ac565f030a9) by [@react-native-bot](https://github.com/react-native-bot))
|
||||
- Release 0.81.0-rc.1 ([b06bb89ddd](https://github.com/facebook/react-native/commit/b06bb89ddd3cebddea4716036a4368b87a65f492) by [@react-native-bot](https://github.com/react-native-bot))
|
||||
|
||||
#### Android Unknown
|
||||
|
||||
|
||||
|
||||
#### iOS Unknown
|
||||
|
||||
|
||||
|
||||
#### Failed to parse
|
||||
|
||||
|
||||
|
||||
- **Podspec:** Fixed issue with RNDeps release/debug switch failing ([4ee2b60a1e](https://github.com/facebook/react-native/commit/4ee2b60a1eacca744d58a7ad336ca9d3714289f6) by [@chrfalch](https://github.com/chrfalch))
|
||||
- **Podspec:** Fixed missing script for resolving prebuilt xcframework when switching between release/debug ([2e55241a90](https://github.com/facebook/react-native/commit/2e55241a901b4cd95917de68ce9078928820a208) by [@chrfalch](https://github.com/chrfalch))
|
||||
|
||||
## v0.81.0-rc.1
|
||||
|
||||
@@ -220,28 +42,27 @@
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- Add release/debug switch script for React-Core-prebuilt ([42d1a7934c](https://github.com/facebook/react-native/commit/42d1a7934cad4b2c92653e3fa7781c2af8f44df4) by [@chrfalch](https://github.com/chrfalch))
|
||||
- Added support for using USE_FRAMEWORKS with prebuilt React Native Core ([40e45f5366](https://github.com/facebook/react-native/commit/40e45f53661ce80c3a6fbbf07f52dc900afcad52) by [@chrfalch](https://github.com/chrfalch))
|
||||
- Add the `ENTERPRISE_REPOSITORY` env variable to cocopaods infra ([23f3bf9239](https://github.com/facebook/react-native/commit/23f3bf9239a849590f1c72b25732d0090780128c) by [@cipolleschi](https://github.com/cipolleschi))
|
||||
- **CocoaPods** Add the `ENTERPRISE_REPOSITORY` env variable to cocoapods infra ([23f3bf9239](https://github.com/facebook/react-native/commit/23f3bf9239a849590f1c72b25732d0090780128c) by [@cipolleschi](https://github.com/cipolleschi))
|
||||
- **Prebuild:** Add release/debug switch script for React-Core-prebuilt ([42d1a7934c](https://github.com/facebook/react-native/commit/42d1a7934cad4b2c92653e3fa7781c2af8f44df4) by [@chrfalch](https://github.com/chrfalch))
|
||||
- **Prebuild:** Added support for using USE_FRAMEWORKS with prebuilt React Native Core ([40e45f5366](https://github.com/facebook/react-native/commit/40e45f53661ce80c3a6fbbf07f52dc900afcad52) by [@chrfalch](https://github.com/chrfalch))
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Metro to 0.83.0 ([6b9f5d622f](https://github.com/facebook/react-native/commit/6b9f5d622ffbe79da8f4e7b7d8094504a480425e) by [@robhogan](https://github.com/robhogan))
|
||||
- **Metro:** Bump Metro to 0.83.0 ([6b9f5d622f](https://github.com/facebook/react-native/commit/6b9f5d622ffbe79da8f4e7b7d8094504a480425e) by [@robhogan](https://github.com/robhogan))
|
||||
|
||||
#### Android specific
|
||||
|
||||
- Gradle to 8.14.3 ([6892dde363](https://github.com/facebook/react-native/commit/6892dde36373bbef2d0afe535ae818b1a7164f08) by [@cortinico](https://github.com/cortinico))
|
||||
- Expose `react_renderer_bridging` headers via prefab ([d1730ff960](https://github.com/facebook/react-native/commit/d1730ff960fcb9a01ee94b9e46e5a9fbb7d73f4a) by [@tomekzaw](https://github.com/tomekzaw))
|
||||
- Introduce more deprecation warnings for Legacy Arch classes ([625f69f284](https://github.com/facebook/react-native/commit/625f69f284ddfd9c6beecaa4052a871d092053ef) by [@cortinico](https://github.com/cortinico))
|
||||
|
||||
- **Gradle:** Gradle to 8.14.3 ([6892dde363](https://github.com/facebook/react-native/commit/6892dde36373bbef2d0afe535ae818b1a7164f08) by [@cortinico](https://github.com/cortinico))
|
||||
- **Gradle:** Expose `react_renderer_bridging` headers via prefab ([d1730ff960](https://github.com/facebook/react-native/commit/d1730ff960fcb9a01ee94b9e46e5a9fbb7d73f4a) by [@tomekzaw](https://github.com/tomekzaw))
|
||||
- **Legacy Arch:** Introduce more deprecation warnings for Legacy Arch classes ([625f69f284](https://github.com/facebook/react-native/commit/625f69f284ddfd9c6beecaa4052a871d092053ef) by [@cortinico](https://github.com/cortinico))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed nodes with `display: contents` set being cloned with the wrong owner ([d4b36b0300](https://github.com/facebook/react-native/commit/d4b36b03003eb2de9eaf5b57bb639bae8cc12f20) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
- **Yoga:** Fixed nodes with `display: contents` set being cloned with the wrong owner ([d4b36b0300](https://github.com/facebook/react-native/commit/d4b36b03003eb2de9eaf5b57bb639bae8cc12f20) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- Fixed premature return in header file generation from podspec globs ([f2b064c2d4](https://github.com/facebook/react-native/commit/f2b064c2d40c39017ac2a31bf3caf8acef23038c) by [@chrfalch](https://github.com/chrfalch))
|
||||
- **Podspec:** Fixed premature return in header file generation from podspec globs ([f2b064c2d4](https://github.com/facebook/react-native/commit/f2b064c2d40c39017ac2a31bf3caf8acef23038c) by [@chrfalch](https://github.com/chrfalch))
|
||||
|
||||
|
||||
## v0.81.0-rc.0
|
||||
@@ -1586,4 +1407,3 @@ See [CHANGELOG-0.5x](./CHANGELOG-0.5x.md#v0530)
|
||||
## v0.52.0
|
||||
|
||||
See [CHANGELOG-0.5x](./CHANGELOG-0.5x.md#v0520)
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ try {
|
||||
} catch (e) {
|
||||
// Fallback to lib when source doesn't exit (e.g. when installed as a dev dependency)
|
||||
FlowParser =
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[cannot-resolve-module]
|
||||
require('@react-native/codegen/lib/parsers/flow/parser').FlowParser;
|
||||
TypeScriptParser =
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[cannot-resolve-module]
|
||||
require('@react-native/codegen/lib/parsers/typescript/parser').TypeScriptParser;
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[cannot-resolve-module]
|
||||
RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen');
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ try {
|
||||
'@react-native-community/cli-server-api',
|
||||
{paths: [communityCliPath]},
|
||||
);
|
||||
// $FlowIgnore[unsupported-syntax] dynamic import
|
||||
// $FlowFixMe[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) {
|
||||
// $FlowIgnore[cannot-write] Assigning to readonly property
|
||||
// $FlowFixMe[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) {
|
||||
// $FlowIgnore[cannot-write] Assigning to readonly property
|
||||
// $FlowFixMe[cannot-write] Assigning to readonly property
|
||||
metroConfig.server.forwardClientLogs = false;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ async function runServer(
|
||||
}
|
||||
},
|
||||
};
|
||||
// $FlowIgnore[cannot-write] Assigning to readonly property
|
||||
// $FlowFixMe[cannot-write] Assigning to readonly property
|
||||
metroConfig.reporter = reporter;
|
||||
|
||||
await Metro.runServer(metroConfig, {
|
||||
@@ -175,7 +175,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');
|
||||
// $FlowIgnore[unsupported-syntax]
|
||||
// $FlowFixMe[unsupported-syntax]
|
||||
return require(customLogReporterPath);
|
||||
} catch (e) {
|
||||
if (e.code !== 'MODULE_NOT_FOUND') {
|
||||
@@ -183,7 +183,7 @@ function getReporterImpl(
|
||||
}
|
||||
// If that doesn't work, then we next try relative to the cwd, eg:
|
||||
// require('./reporter');
|
||||
// $FlowIgnore[unsupported-syntax]
|
||||
// $FlowFixMe[unsupported-syntax]
|
||||
return require(path.resolve(customLogReporterPath));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const FIRST = 1,
|
||||
FOURTH = 4;
|
||||
|
||||
function getNodePackagePath(packageName: string): string {
|
||||
// $FlowIgnore[prop-missing] type definition is incomplete
|
||||
// $FlowFixMe[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 {
|
||||
// $FlowIgnore[prop-missing] type definition is incomplete
|
||||
// $FlowFixMe[prop-missing] type definition is incomplete
|
||||
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@generated SignedSource<<74c5fb174ae5a8a3850a3c2373b3b6f5>>
|
||||
Git revision: a7e4f59675edbda995b0cb0d40f277a59a3baebf
|
||||
@generated SignedSource<<9252db36d4b1db907a38c08935ceeb38>>
|
||||
Git revision: 921566790e9e16d0ecace6e49b3cfaace205958c
|
||||
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
@@ -16,7 +16,7 @@ contextBridge.executeInMainWorld({
|
||||
let didDecorateInspectorFrontendHostInstance = false;
|
||||
// reactNativeDecorateInspectorFrontendHostInstance was introduced in
|
||||
// https://github.com/facebook/react-native-devtools-frontend/pull/168
|
||||
// $FlowIgnore[prop-missing]
|
||||
// $FlowFixMe[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) {
|
||||
// $FlowIgnore[invalid-export]
|
||||
// $FlowFixMe[invalid-export]
|
||||
module.exports = require('./electron');
|
||||
} else {
|
||||
// $FlowIgnore[invalid-export]
|
||||
// $FlowFixMe[invalid-export]
|
||||
module.exports = require('./node');
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class DebuggerAgent {
|
||||
this.#ws = null;
|
||||
}
|
||||
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[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;
|
||||
// $FlowIgnore[incompatible-use]
|
||||
// $FlowIgnore[prop-missing]
|
||||
// $FlowFixMe[incompatible-use]
|
||||
// $FlowFixMe[prop-missing]
|
||||
const [response] = newHandleCalls.find(args => args[0].id === message.id);
|
||||
// $FlowIgnore[incompatible-return]
|
||||
// $FlowIgnore[incompatible-indexer]
|
||||
// $FlowFixMe[incompatible-return]
|
||||
// $FlowFixMe[incompatible-indexer]
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class DeviceAgent {
|
||||
});
|
||||
}
|
||||
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[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;
|
||||
// $FlowIgnore[incompatible-type]
|
||||
// $FlowFixMe[incompatible-type]
|
||||
const [receivedMessage]: [Message] = newHandleCalls.find(
|
||||
// $FlowIgnore[incompatible-call]
|
||||
// $FlowFixMe[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;
|
||||
// $FlowIgnore[incompatible-use]
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const [receivedMessage] = newEventCalls.find(
|
||||
// $FlowIgnore[prop-missing]
|
||||
// $FlowIgnore[incompatible-use]
|
||||
// $FlowFixMe[prop-missing]
|
||||
// $FlowFixMe[incompatible-use]
|
||||
call => call[0].wrappedEvent.id === message.id,
|
||||
);
|
||||
// $FlowIgnore[incompatible-return]
|
||||
// $FlowFixMe[incompatible-return]
|
||||
return receivedMessage.wrappedEvent;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export async function createAndConnectTarget(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toHaveLength(1);
|
||||
});
|
||||
@@ -119,7 +119,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toHaveLength(1);
|
||||
});
|
||||
@@ -187,7 +187,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toHaveLength(1);
|
||||
});
|
||||
@@ -288,7 +288,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toHaveLength(1);
|
||||
});
|
||||
@@ -338,7 +338,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
await until(async () => {
|
||||
pageList = (await fetchJson(
|
||||
`${serverRef.serverBaseUrl}/json`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[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`,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
): any);
|
||||
expect(pageList).toContainEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
export function withAbortSignalForEachTest(): $ReadOnly<{signal: AbortSignal}> {
|
||||
const ref: {signal: AbortSignal} = {
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[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,
|
||||
} = {
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[unsafe-getters-setters]
|
||||
get serverBaseUrl() {
|
||||
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
|
||||
},
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[unsafe-getters-setters]
|
||||
get serverBaseWsUrl() {
|
||||
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
|
||||
},
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[unsafe-getters-setters]
|
||||
get app() {
|
||||
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
|
||||
},
|
||||
// $FlowIgnore[unsafe-getters-setters]
|
||||
// $FlowFixMe[unsafe-getters-setters]
|
||||
get port() {
|
||||
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ function makeRequest(
|
||||
host: ?string,
|
||||
encrypted: boolean,
|
||||
): http$IncomingMessage<> | http$IncomingMessage<tls$TLSSocket> {
|
||||
// $FlowIgnore[incompatible-return] Partial mock of request
|
||||
// $FlowFixMe[incompatible-return] Partial mock of request
|
||||
return {
|
||||
socket: encrypted ? {encrypted: true} : {},
|
||||
headers: host != null ? {host} : {},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.11.0"
|
||||
agp = "8.12.0"
|
||||
gson = "2.8.9"
|
||||
guava = "31.0.1-jre"
|
||||
javapoet = "1.13.0"
|
||||
|
||||
+37
-14
@@ -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,6 +43,7 @@ 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
|
||||
@@ -115,6 +116,30 @@ 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 {
|
||||
@@ -271,19 +296,17 @@ class ReactPlugin : Plugin<Project> {
|
||||
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
+1
-5
@@ -13,7 +13,6 @@ 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
|
||||
@@ -65,10 +64,7 @@ internal object AgpConfiguratorUtils {
|
||||
.getByType(ApplicationAndroidComponentsExtension::class.java)
|
||||
.finalizeDsl { ext ->
|
||||
ext.buildFeatures.buildConfig = true
|
||||
ext.defaultConfig.buildConfigField(
|
||||
"boolean",
|
||||
"IS_NEW_ARCHITECTURE_ENABLED",
|
||||
project.isNewArchEnabled(extension).toString())
|
||||
ext.defaultConfig.buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
|
||||
ext.defaultConfig.buildConfigField(
|
||||
"boolean", "IS_HERMES_ENABLED", project.isHermesEnabled.toString())
|
||||
ext.defaultConfig.buildConfigField(
|
||||
|
||||
+13
-28
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -21,10 +20,6 @@ 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
|
||||
@@ -78,29 +73,19 @@ internal object NdkConfiguratorUtils {
|
||||
extension: ReactExtension,
|
||||
variant: Variant
|
||||
) {
|
||||
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",
|
||||
))
|
||||
}
|
||||
// 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",
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-8
@@ -13,11 +13,9 @@ 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
|
||||
@@ -28,12 +26,7 @@ internal object ProjectUtils {
|
||||
|
||||
const val HERMES_FALLBACK = 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 fun Project.isNewArchEnabled(): Boolean = true
|
||||
|
||||
internal val Project.isHermesEnabled: Boolean
|
||||
get() =
|
||||
|
||||
+2
-64
@@ -27,70 +27,8 @@ class ProjectUtilsTest {
|
||||
@get:Rule val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
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()
|
||||
fun isNewArchEnabled_alwaysReturnsTrue() {
|
||||
assertThat(createProject().isNewArchEnabled()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"glob": "^7.1.1",
|
||||
"hermes-parser": "0.30.0",
|
||||
"invariant": "^2.2.4",
|
||||
|
||||
+1
-1
@@ -487,7 +487,7 @@ describe('buildSchemaFromConfigType', () => {
|
||||
|
||||
describe('when buildModuleSchema returns null', () => {
|
||||
it('throws an error', () => {
|
||||
// $FlowIgnore[incompatible-call] - This is to test an invariant
|
||||
// $FlowFixMe[incompatible-call] - This is to test an invariant
|
||||
buildModuleSchemaMock.mockReturnValueOnce(null);
|
||||
|
||||
expect(() =>
|
||||
|
||||
Vendored
+2
-2
@@ -10,9 +10,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[cannot-resolve-module]
|
||||
const flowSnaps = require('../../../../src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap');
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[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';
|
||||
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[cannot-resolve-module]
|
||||
const flowSnaps = require('../../../../src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap');
|
||||
// $FlowIgnore[cannot-resolve-module]
|
||||
// $FlowFixMe[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');
|
||||
|
||||
@@ -48,6 +48,7 @@ const SUPPORTED_STYLES: {[string]: true} = {
|
||||
borderStartStartRadius: true,
|
||||
elevation: true,
|
||||
opacity: true,
|
||||
filter: true,
|
||||
transform: true,
|
||||
zIndex: true,
|
||||
/* ios styles */
|
||||
|
||||
@@ -21,11 +21,11 @@ function mockQueueMicrotask() {
|
||||
let queueMicrotask;
|
||||
beforeEach(() => {
|
||||
queueMicrotask = global.queueMicrotask;
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.queueMicrotask = process.nextTick;
|
||||
});
|
||||
afterEach(() => {
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.queueMicrotask = queueMicrotask;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ function processColor(
|
||||
}
|
||||
|
||||
if (isRgbaValue(color)) {
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[incompatible-cast] - Type is verified above
|
||||
return (color: RgbaValue);
|
||||
}
|
||||
|
||||
let normalizedColor: ?ProcessedColorValue = normalizeColor(
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[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)) {
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[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 =
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[incompatible-cast] - Type is verified above
|
||||
processColor((value: ColorValue | RgbaValue)) ?? defaultColor;
|
||||
let initColor: RgbaValue = defaultColor;
|
||||
if (isRgbaValue(processedColor)) {
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[incompatible-cast] - Type is verified above
|
||||
initColor = (processedColor: RgbaValue);
|
||||
} else {
|
||||
// $FlowIgnore[incompatible-cast] - Type is verified above
|
||||
// $FlowFixMe[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)) {
|
||||
// $FlowIgnore[incompatible-type] - Type is verified above
|
||||
// $FlowFixMe[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 {
|
||||
// $FlowIgnore[incompatible-type] - Type is verified above
|
||||
// $FlowFixMe[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) =>
|
||||
// $FlowIgnoreMe[invalid-compare]
|
||||
// $FlowFixMe[invalid-compare]
|
||||
typeof component === 'number' || component === firstOutput[i],
|
||||
),
|
||||
),
|
||||
@@ -235,9 +235,9 @@ function createStringInterpolation(
|
||||
const numericComponents: $ReadOnlyArray<$ReadOnlyArray<number>> =
|
||||
outputRange.map(output =>
|
||||
isColor
|
||||
? // $FlowIgnoreMe[incompatible-call]
|
||||
? // $FlowFixMe[incompatible-type]
|
||||
output.components
|
||||
: // $FlowIgnoreMe[incompatible-call]
|
||||
: // $FlowFixMe[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') {
|
||||
// $FlowIgnoreMe[incompatible-cast]
|
||||
// $FlowFixMe[incompatible-cast]
|
||||
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
|
||||
const processedColor = processColor(value);
|
||||
if (typeof processedColor === 'number') {
|
||||
|
||||
@@ -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.
|
||||
// $FlowIgnore[method-unbinding]
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const _hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
|
||||
// $FlowIgnore[method-unbinding]
|
||||
// $FlowFixMe[method-unbinding]
|
||||
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
|
||||
|
||||
@@ -123,7 +123,7 @@ export default class AnimatedStyle extends AnimatedWithChildren {
|
||||
this._style = style;
|
||||
|
||||
if ((Platform.OS as string) === 'web') {
|
||||
// $FlowIgnore[cannot-write] - Intentional shadowing.
|
||||
// $FlowFixMe[cannot-write] - Intentional shadowing.
|
||||
this.__getValueForStyle = resultStyle => [
|
||||
originalStyleForWeb,
|
||||
resultStyle,
|
||||
@@ -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.
|
||||
// $FlowIgnore[method-unbinding]
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const _hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
|
||||
// $FlowIgnore[method-unbinding]
|
||||
// $FlowFixMe[method-unbinding]
|
||||
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
|
||||
{
|
||||
RCTEnableTurboModule(turboModuleEnabled);
|
||||
RCTEnableTurboModule(YES);
|
||||
|
||||
#if DEBUG
|
||||
// Disable idle timer in dev builds to avoid putting application in background and complicating
|
||||
@@ -43,15 +43,12 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
|
||||
UIView *
|
||||
RCTAppSetupDefaultRootView(RCTBridge *bridge, NSString *moduleName, NSDictionary *initialProperties, BOOL fabricEnabled)
|
||||
{
|
||||
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];
|
||||
id<RCTSurfaceProtocol> surface = [[RCTFabricSurface alloc] initWithBridge:bridge
|
||||
moduleName:moduleName
|
||||
initialProperties:initialProperties];
|
||||
UIView *rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
|
||||
[surface start];
|
||||
return rootView;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *RCTAppSetupUnstableModulesRequiringMainQueueSetup(id<RCTDependencyProvider> dependencyProvider)
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
moduleName:(NSString *)moduleName
|
||||
initProps:(NSDictionary *)initProps
|
||||
{
|
||||
BOOL enableFabric = self.fabricEnabled;
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, YES);
|
||||
|
||||
rootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
|
||||
@@ -107,22 +106,22 @@
|
||||
|
||||
- (BOOL)newArchEnabled
|
||||
{
|
||||
return RCTIsNewArchEnabled();
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)bridgelessEnabled
|
||||
{
|
||||
return self.newArchEnabled;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fabricEnabled
|
||||
{
|
||||
return self.newArchEnabled;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)turboModuleEnabled
|
||||
{
|
||||
return self.newArchEnabled;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (Class)getModuleClassFromName:(const char *)name
|
||||
|
||||
@@ -52,17 +52,12 @@ using namespace facebook::react;
|
||||
self.delegate = delegate;
|
||||
[self _setUpFeatureFlags:releaseLevel];
|
||||
|
||||
auto newArchEnabled = [self newArchEnabled];
|
||||
auto fabricEnabled = [self fabricEnabled];
|
||||
|
||||
[RCTColorSpaceUtils applyDefaultColorSpace:[self defaultColorSpace]];
|
||||
RCTEnableTurboModule([self turboModuleEnabled]);
|
||||
RCTEnableTurboModule(YES);
|
||||
|
||||
self.rootViewFactory = [self createRCTRootViewFactory];
|
||||
|
||||
if (newArchEnabled || fabricEnabled) {
|
||||
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
|
||||
}
|
||||
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -126,37 +121,22 @@ using namespace facebook::react;
|
||||
|
||||
- (BOOL)newArchEnabled
|
||||
{
|
||||
if ([_delegate respondsToSelector:@selector(newArchEnabled)]) {
|
||||
return _delegate.newArchEnabled;
|
||||
}
|
||||
return RCTIsNewArchEnabled();
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fabricEnabled
|
||||
{
|
||||
if ([_delegate respondsToSelector:@selector(fabricEnabled)]) {
|
||||
return _delegate.fabricEnabled;
|
||||
}
|
||||
|
||||
return [self newArchEnabled];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)turboModuleEnabled
|
||||
{
|
||||
if ([_delegate respondsToSelector:@selector(turboModuleEnabled)]) {
|
||||
return _delegate.turboModuleEnabled;
|
||||
}
|
||||
|
||||
return [self newArchEnabled];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)bridgelessEnabled
|
||||
{
|
||||
if ([_delegate respondsToSelector:@selector(bridgelessEnabled)]) {
|
||||
return _delegate.bridgelessEnabled;
|
||||
}
|
||||
|
||||
return [self newArchEnabled];
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - RCTTurboModuleManagerDelegate
|
||||
@@ -250,9 +230,9 @@ using namespace facebook::react;
|
||||
|
||||
RCTRootViewFactoryConfiguration *configuration =
|
||||
[[RCTRootViewFactoryConfiguration alloc] initWithBundleURLBlock:bundleUrlBlock
|
||||
newArchEnabled:self.fabricEnabled
|
||||
turboModuleEnabled:self.turboModuleEnabled
|
||||
bridgelessEnabled:self.bridgelessEnabled];
|
||||
newArchEnabled:YES
|
||||
turboModuleEnabled:YES
|
||||
bridgelessEnabled:YES];
|
||||
|
||||
configuration.createRootViewWithBridge = ^UIView *(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps) {
|
||||
return [weakSelf.delegate createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
|
||||
@@ -334,9 +314,7 @@ using namespace facebook::react;
|
||||
dispatch_once(&setupFeatureFlagsToken, ^{
|
||||
switch (releaseLevel) {
|
||||
case Stable:
|
||||
if ([self bridgelessEnabled]) {
|
||||
ReactNativeFeatureFlags::override(std::make_unique<ReactNativeFeatureFlagsOverridesOSSStable>());
|
||||
}
|
||||
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 = newArchEnabled;
|
||||
_turboModuleEnabled = turboModuleEnabled;
|
||||
_bridgelessEnabled = bridgelessEnabled;
|
||||
_fabricEnabled = YES;
|
||||
_turboModuleEnabled = YES;
|
||||
_bridgelessEnabled = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -135,17 +135,12 @@
|
||||
|
||||
- (void)initializeReactHostWithLaunchOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
if (_configuration.bridgelessEnabled) {
|
||||
// Enable TurboModule interop by default in Bridgeless mode
|
||||
RCTEnableTurboModuleInterop(YES);
|
||||
RCTEnableTurboModuleInteropBridgeProxy(YES);
|
||||
// Enable TurboModule interop by default in Bridgeless mode
|
||||
RCTEnableTurboModuleInterop(YES);
|
||||
RCTEnableTurboModuleInteropBridgeProxy(YES);
|
||||
|
||||
[self createReactHostIfNeeded:launchOptions];
|
||||
return;
|
||||
}
|
||||
|
||||
[self createBridgeIfNeeded:launchOptions];
|
||||
[self createBridgeAdapterIfNeeded];
|
||||
[self createReactHostIfNeeded:launchOptions];
|
||||
return;
|
||||
}
|
||||
|
||||
- (UIView *)viewWithModuleName:(NSString *)moduleName
|
||||
@@ -154,29 +149,17 @@
|
||||
{
|
||||
[self initializeReactHostWithLaunchOptions:launchOptions];
|
||||
|
||||
if (_configuration.bridgelessEnabled) {
|
||||
RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName initialProperties:initProps];
|
||||
RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName
|
||||
initialProperties:initProps ? initProps : @{}];
|
||||
|
||||
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView =
|
||||
[[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
|
||||
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView =
|
||||
[[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
|
||||
|
||||
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
if (_configuration.customizeRootView != nil) {
|
||||
_configuration.customizeRootView(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];
|
||||
}
|
||||
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
if (_configuration.customizeRootView != nil) {
|
||||
_configuration.customizeRootView(rootView);
|
||||
_configuration.customizeRootView(surfaceHostingProxyRootView);
|
||||
}
|
||||
return rootView;
|
||||
return surfaceHostingProxyRootView;
|
||||
}
|
||||
|
||||
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions
|
||||
@@ -188,8 +171,7 @@
|
||||
moduleName:(NSString *)moduleName
|
||||
initProps:(NSDictionary *)initProps
|
||||
{
|
||||
BOOL enableFabric = _configuration.fabricEnabled;
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
|
||||
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, YES);
|
||||
rootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
return rootView;
|
||||
}
|
||||
@@ -198,19 +180,15 @@
|
||||
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
_runtimeScheduler = std::make_shared<facebook::react::RuntimeScheduler>(RCTRuntimeExecutorFromBridge(bridge));
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
- (void)createBridgeIfNeeded:(NSDictionary *)launchOptions
|
||||
@@ -228,7 +206,7 @@
|
||||
|
||||
- (void)createBridgeAdapterIfNeeded
|
||||
{
|
||||
if (!self->_configuration.fabricEnabled || self.bridgeAdapter) {
|
||||
if (self.bridgeAdapter != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -618,6 +618,9 @@ 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 ||
|
||||
@@ -681,6 +684,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
|
||||
{...otherProps}
|
||||
{...eventHandlers}
|
||||
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessible={accessible}
|
||||
submitBehavior={submitBehavior}
|
||||
@@ -744,8 +748,9 @@ function InternalTextInput(props: TextInputProps): React.Node {
|
||||
{...otherProps}
|
||||
{...colorProps}
|
||||
{...eventHandlers}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityLabelledBy={_accessibilityLabelledBy}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessible={accessible}
|
||||
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
|
||||
autoCapitalize={autoCapitalize}
|
||||
|
||||
+373
-182
@@ -10,213 +10,404 @@
|
||||
|
||||
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
|
||||
|
||||
import type {HostInstance} from 'react-native';
|
||||
import type {TextInputInstance} from '../TextInput.flow';
|
||||
|
||||
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('focus view command', () => {
|
||||
it('creates view before dispatching view command from ref function', () => {
|
||||
const root = Fantom.createRoot();
|
||||
describe('<TextInput>', () => {
|
||||
describe('props', () => {
|
||||
describe('selection', () => {
|
||||
it('the selection is passed to component view by command', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<TextInput
|
||||
nativeID="text-input"
|
||||
ref={node => {
|
||||
if (node) {
|
||||
node.focus();
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
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: "focus"}',
|
||||
]);
|
||||
});
|
||||
|
||||
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"}',
|
||||
]);
|
||||
});
|
||||
|
||||
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.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"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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} />,
|
||||
);
|
||||
});
|
||||
|
||||
const element = ensureInstance(nodeRef.current, ReactNativeElement);
|
||||
|
||||
expect(focusEvent).toHaveBeenCalledTimes(0);
|
||||
expect(blurEvent).toHaveBeenCalledTimes(0);
|
||||
|
||||
Fantom.runOnUIThread(() => {
|
||||
Fantom.enqueueNativeEvent(element, 'focus');
|
||||
});
|
||||
|
||||
// The tasks have not run.
|
||||
expect(focusEvent).toHaveBeenCalledTimes(0);
|
||||
expect(blurEvent).toHaveBeenCalledTimes(0);
|
||||
|
||||
Fantom.runWorkLoop();
|
||||
|
||||
expect(focusEvent).toHaveBeenCalledTimes(1);
|
||||
expect(blurEvent).toHaveBeenCalledTimes(0);
|
||||
|
||||
Fantom.runOnUIThread(() => {
|
||||
Fantom.enqueueNativeEvent(element, 'blur');
|
||||
});
|
||||
|
||||
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}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const element = ensureInstance(nodeRef.current, ReactNativeElement);
|
||||
|
||||
Fantom.runOnUIThread(() => {
|
||||
Fantom.enqueueNativeEvent(element, 'change', {
|
||||
text: 'Hello World',
|
||||
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.runWorkLoop();
|
||||
describe('onChange', () => {
|
||||
it('is called when the change native event is dispatched', () => {
|
||||
const root = Fantom.createRoot();
|
||||
const nodeRef = createRef<TextInputInstance>();
|
||||
const onChange = jest.fn();
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
const [entry] = onChange.mock.lastCall;
|
||||
expect(entry.text).toEqual('Hello World');
|
||||
});
|
||||
});
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<TextInput
|
||||
onChange={event => {
|
||||
onChange(event.nativeEvent);
|
||||
}}
|
||||
ref={nodeRef}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
describe('onChangeText', () => {
|
||||
it('delivers onChangeText event', () => {
|
||||
const root = Fantom.createRoot();
|
||||
const nodeRef = createRef<HostInstance>();
|
||||
const onChangeText = jest.fn();
|
||||
const element = ensureInstance(nodeRef.current, ReactNativeElement);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<TextInput onChangeText={onChangeText} ref={nodeRef} />);
|
||||
});
|
||||
Fantom.runOnUIThread(() => {
|
||||
Fantom.enqueueNativeEvent(element, 'change', {
|
||||
text: 'Hello World',
|
||||
});
|
||||
});
|
||||
|
||||
const element = ensureInstance(nodeRef.current, ReactNativeElement);
|
||||
Fantom.runWorkLoop();
|
||||
|
||||
Fantom.runOnUIThread(() => {
|
||||
Fantom.enqueueNativeEvent(element, 'change', {
|
||||
text: 'Hello World',
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
const [entry] = onChange.mock.lastCall;
|
||||
expect(entry.text).toEqual('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
Fantom.runWorkLoop();
|
||||
describe('onChangeText', () => {
|
||||
it('is called when the change native event is dispatched', () => {
|
||||
const root = Fantom.createRoot();
|
||||
const nodeRef = createRef<TextInputInstance>();
|
||||
const onChangeText = jest.fn();
|
||||
|
||||
expect(onChangeText).toHaveBeenCalledTimes(1);
|
||||
const [entry] = onChangeText.mock.lastCall;
|
||||
expect(entry).toEqual('Hello World');
|
||||
});
|
||||
});
|
||||
Fantom.runTask(() => {
|
||||
root.render(<TextInput onChangeText={onChangeText} ref={nodeRef} />);
|
||||
});
|
||||
|
||||
describe('props.selection', () => {
|
||||
it('the selection is passed to component view by command', () => {
|
||||
const root = Fantom.createRoot();
|
||||
const element = ensureInstance(nodeRef.current, ReactNativeElement);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<TextInput nativeID="text-input" selection={{start: 0, end: 4}}>
|
||||
hello World!
|
||||
</TextInput>,
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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]"}',
|
||||
]);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ref', () => {
|
||||
it('is an element node', () => {
|
||||
const ref = createRef<TextInputInstance>();
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<TextInput ref={ref} />);
|
||||
});
|
||||
|
||||
expect(ref.current).toBeInstanceOf(ReactNativeElement);
|
||||
});
|
||||
|
||||
it('provides additional methods: clear, isFocused, getNativeRef, setSelection', () => {
|
||||
const ref = createRef<TextInputInstance>();
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<TextInput ref={ref} />);
|
||||
});
|
||||
|
||||
const instance = nullthrows(ref.current);
|
||||
expect(instance.clear).toBeInstanceOf(Function);
|
||||
expect(instance.isFocused).toBeInstanceOf(Function);
|
||||
expect(instance.getNativeRef).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
describe('focus()', () => {
|
||||
it('dispatches the focus command', () => {
|
||||
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.focus();
|
||||
});
|
||||
|
||||
expect(root.takeMountingManagerLogs()).toEqual([
|
||||
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
|
||||
]);
|
||||
});
|
||||
|
||||
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"}',
|
||||
]);
|
||||
});
|
||||
|
||||
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"}',
|
||||
]);
|
||||
});
|
||||
|
||||
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.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"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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.runTask(() => {
|
||||
instance.focus();
|
||||
});
|
||||
|
||||
root.takeMountingManagerLogs();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
instance.blur();
|
||||
});
|
||||
|
||||
expect(root.takeMountingManagerLogs()).toEqual([
|
||||
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "blur"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
instance.clear();
|
||||
});
|
||||
|
||||
expect(root.takeMountingManagerLogs()).toEqual([
|
||||
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "setTextAndSelection, args: [0,"",0,0]"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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.runTask(() => {
|
||||
instance.focus();
|
||||
});
|
||||
|
||||
expect(instance.isFocused()).toBe(true);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
instance.blur();
|
||||
});
|
||||
|
||||
expect(instance.isFocused()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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]"}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
@@ -432,6 +432,7 @@ jest.unmock('../TextInput');
|
||||
|
||||
expect(instance.toJSON()).toMatchInlineSnapshot(`
|
||||
<RCTSinglelineTextInputView
|
||||
accessibilityLabel="label"
|
||||
accessibilityState={
|
||||
Object {
|
||||
"busy": true,
|
||||
|
||||
@@ -477,7 +477,7 @@ function runExceptionsManagerTests() {
|
||||
expect(nativeReportException).not.toBeCalled();
|
||||
expect(logBoxAddConsoleLog).toBeCalledTimes(1);
|
||||
expect(logBoxAddConsoleLog.mock.calls[0][0]).toBe('error');
|
||||
// $FlowIgnore[incompatible-call]
|
||||
// $FlowFixMe[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',
|
||||
};
|
||||
// $FlowIgnore[prop-missing]
|
||||
// $FlowFixMe[prop-missing]
|
||||
object.cycle = object;
|
||||
|
||||
const args = [object];
|
||||
|
||||
+2
-2
@@ -27,12 +27,12 @@ function _setDevelopmentModeForTests(dev: mixed) {
|
||||
|
||||
beforeAll(() => {
|
||||
originalDev = global.__DEV__;
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.__DEV__ = dev;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.__DEV__ = originalDev;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ import {getUrlCacheBreaker, setUrlCacheBreaker} from '../AssetUtils';
|
||||
|
||||
describe('AssetUtils', () => {
|
||||
afterEach(() => {
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.__DEV__ = true;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty string and warn once if no cacheBreaker set (DEV)', () => {
|
||||
const mockWarn = jest.spyOn(console, 'warn').mockReturnValue(undefined);
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.__DEV__ = true;
|
||||
expect(getUrlCacheBreaker()).toEqual('');
|
||||
expect(getUrlCacheBreaker()).toEqual('');
|
||||
@@ -28,7 +28,7 @@ describe('AssetUtils', () => {
|
||||
|
||||
it('should return empty string if no cacheBreaker set in prod', () => {
|
||||
const mockWarn = jest.spyOn(console, 'warn');
|
||||
// $FlowIgnore[cannot-write]
|
||||
// $FlowFixMe[cannot-write]
|
||||
global.__DEV__ = false;
|
||||
expect(getUrlCacheBreaker()).toEqual('');
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
/**
|
||||
* 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 type {HostInstance} from 'react-native';
|
||||
|
||||
import * as Fantom from '@react-native/fantom';
|
||||
import * as React from 'react';
|
||||
import {createRef} from 'react';
|
||||
import {Image} from 'react-native';
|
||||
import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureInstance';
|
||||
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
|
||||
|
||||
const LOGO_SOURCE = {uri: 'https://reactnative.dev/img/tiny_logo.png'};
|
||||
|
||||
describe('<Image>', () => {
|
||||
describe('props', () => {
|
||||
describe('empty props', () => {
|
||||
// TODO T233552213: do not send empty source
|
||||
it('renders an empty element when there are no props', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput().toJSX()).toEqual(
|
||||
<rn-image overflow="hidden" source-scale="1" source-type="remote" />,
|
||||
);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image src="" />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput().toJSX()).toEqual(
|
||||
<rn-image overflow="hidden" source-scale="1" source-type="remote" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessibility', () => {
|
||||
describe('accessible', () => {
|
||||
it('indicates that image is an accessibility element', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image accessible={true} />);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['accessible']}).toJSX(),
|
||||
).toEqual(<rn-image accessible="true" />);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessibilityLabel', () => {
|
||||
it('provides information for screen reader', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image accessibilityLabel="React Native Logo" />);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(),
|
||||
).toEqual(<rn-image accessibilityLabel="React Native Logo" />);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alt', () => {
|
||||
it('provides information for screen reader', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image alt="React Native Logo" />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['^access']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
accessible="true"
|
||||
accessibilityLabel="React Native Logo"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can be set alongside accessibilityLabel, but accessibilityLabel has higher priority', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
alt="React Native Logo"
|
||||
accessibilityLabel="React Native"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['^access']}).toJSX()).toEqual(
|
||||
<rn-image accessible="true" accessibilityLabel="React Native" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('blurRadius', () => {
|
||||
it('provides blur radius for image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image blurRadius={10} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['blurRadius']}).toJSX()).toEqual(
|
||||
<rn-image blurRadius="10" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crossOrigin', () => {
|
||||
it('does not set any headers in anonymous mode', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-type="remote"
|
||||
source-uri={LOGO_SOURCE.uri}
|
||||
/>,
|
||||
);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image crossOrigin="anonymous" source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-type="remote"
|
||||
source-uri={LOGO_SOURCE.uri}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the "Access-Control-Allow-Credentials" header in "use-credentials" mode', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image crossOrigin="use-credentials" source={LOGO_SOURCE} />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['source-header']}).toJSX(),
|
||||
).toEqual(
|
||||
<rn-image source-header-Access-Control-Allow-Credentials="true" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultSource', () => {
|
||||
it('can provide a default image to display', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
defaultSource={require('./img/img1.png')}
|
||||
source={LOGO_SOURCE}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['defaultSource']}).toJSX(),
|
||||
).toEqual(
|
||||
<rn-image
|
||||
defaultSource-type="remote"
|
||||
defaultSource-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('height', () => {
|
||||
it('provides height for image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image height={100} source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
|
||||
<rn-image height="100.000000" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('width', () => {
|
||||
it('provides width for image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image width={100} source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
|
||||
<rn-image width="100.000000" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading progress', () => {
|
||||
(
|
||||
[
|
||||
['onError', 'fails to load'],
|
||||
['onLoadStart', 'start loading'],
|
||||
['onProgress', 'is loading'],
|
||||
['onLoad', 'loads successfully'],
|
||||
['onLoadEnd', 'ends loading'],
|
||||
] as const
|
||||
).forEach(([onProp, event]) => {
|
||||
it(`${onProp} is called when image ${event}`, () => {
|
||||
const onPropCallback = jest.fn();
|
||||
const ref = createRef<HostInstance>();
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
ref={ref}
|
||||
source={LOGO_SOURCE}
|
||||
onError={() => {
|
||||
onProp === 'onError' && onPropCallback();
|
||||
}}
|
||||
onLoad={() => {
|
||||
onProp === 'onLoad' && onPropCallback();
|
||||
}}
|
||||
onLoadStart={() => {
|
||||
onProp === 'onLoadStart' && onPropCallback();
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
onProp === 'onLoadEnd' && onPropCallback();
|
||||
}}
|
||||
onProgress={() => {
|
||||
onProp === 'onProgress' && onPropCallback();
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(onPropCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
const image = ensureInstance(ref.current, ReactNativeElement);
|
||||
Fantom.dispatchNativeEvent(image, onProp, {});
|
||||
|
||||
expect(onPropCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('referrerPolicy', () => {
|
||||
(
|
||||
[
|
||||
'no-referrer',
|
||||
'no-referrer-when-downgrade',
|
||||
'origin',
|
||||
'origin-when-cross-origin',
|
||||
'same-origin',
|
||||
'strict-origin',
|
||||
'strict-origin-when-cross-origin',
|
||||
'unsafe-url',
|
||||
] as const
|
||||
).forEach(referrerPolicy => {
|
||||
it(`${referrerPolicy} sets correct "Referrer-Policy" header`, () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image referrerPolicy={referrerPolicy} src={LOGO_SOURCE.uri} />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['source-header']}).toJSX(),
|
||||
).toEqual(
|
||||
<rn-image source-header-Referrer-Policy={referrerPolicy} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resizeMode', () => {
|
||||
it('is set to "cover" by default', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['resizeMode']}).toJSX()).toEqual(
|
||||
<rn-image />,
|
||||
);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image resizeMode="cover" source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['resizeMode']}).toJSX()).toEqual(
|
||||
<rn-image />,
|
||||
);
|
||||
});
|
||||
|
||||
(['stretch', 'contain', 'repeat', 'center'] as const).forEach(
|
||||
resizeMode => {
|
||||
it(`can be set to "${resizeMode}"`, () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image resizeMode={resizeMode} source={LOGO_SOURCE} />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['resizeMode']}).toJSX(),
|
||||
).toEqual(<rn-image resizeMode={resizeMode} />);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('source', () => {
|
||||
it('can be set to a local image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image source={require('./img/img1.png')} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-size="{1, 1}"
|
||||
source-type="local"
|
||||
source-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can be set to a remote image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://reactnative.dev/img/tiny_logo.png',
|
||||
width: 100,
|
||||
height: 100,
|
||||
scale: 2,
|
||||
cache: 'only-if-cached',
|
||||
method: 'POST',
|
||||
body: 'name=React+Native',
|
||||
headers: {
|
||||
Authorization: 'Basic RandomString',
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-body="name=React+Native"
|
||||
source-cache="only-if-cached"
|
||||
source-header-Authorization="Basic RandomString"
|
||||
source-method="POST"
|
||||
source-scale="2"
|
||||
source-size="{100, 100}"
|
||||
source-type="remote"
|
||||
source-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can be set to a list of remote images', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
source={[
|
||||
{
|
||||
uri: 'https://reactnative.dev/img/tiny_logo.png',
|
||||
scale: 1,
|
||||
headers: {
|
||||
Authorization: 'Basic RandomString',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'https://reactnative.dev/img/medium_logo.png',
|
||||
scale: 2,
|
||||
cache: 'only-if-cached',
|
||||
},
|
||||
{
|
||||
uri: 'https://reactnative.dev/img/large_logo.png',
|
||||
scale: 3,
|
||||
method: 'POST',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-1x-header-Authorization="Basic RandomString"
|
||||
source-1x-scale="1"
|
||||
source-1x-type="remote"
|
||||
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
source-2x-cache="only-if-cached"
|
||||
source-2x-scale="2"
|
||||
source-2x-type="remote"
|
||||
source-2x-uri="https://reactnative.dev/img/medium_logo.png"
|
||||
source-3x-method="POST"
|
||||
source-3x-type="remote"
|
||||
source-3x-uri="https://reactnative.dev/img/large_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('src', () => {
|
||||
it('can be set to a remote image', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image src="https://reactnative.dev/img/tiny_logo.png" />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-type="remote"
|
||||
source-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('takes precedence over `source` prop', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
src="https://reactnative.dev/img/tiny_logo.png"
|
||||
source={{uri: 'https://reactnative.dev/img/medium_logo.png'}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-type="remote"
|
||||
source-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('srcSet', () => {
|
||||
it('can be set to a list of remote images', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
srcSet={
|
||||
'https://reactnative.dev/img/tiny_logo.png 1x, https://reactnative.dev/img/header_logo.svg 2x'
|
||||
}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-1x-scale="1"
|
||||
source-1x-type="remote"
|
||||
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
source-2x-scale="2"
|
||||
source-2x-type="remote"
|
||||
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to `1x` descriptor', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
srcSet={
|
||||
'https://reactnative.dev/img/tiny_logo.png, https://reactnative.dev/img/header_logo.svg 2x'
|
||||
}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-1x-scale="1"
|
||||
source-1x-type="remote"
|
||||
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
source-2x-scale="2"
|
||||
source-2x-type="remote"
|
||||
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses `src` for `1x` descriptor when provided', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
srcSet={
|
||||
'https://reactnative.dev/img/header_logo.svg 2x, https://reactnative.dev/img/large_logo.svg 3x'
|
||||
}
|
||||
src="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-1x-scale="1"
|
||||
source-1x-type="remote"
|
||||
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
source-2x-scale="2"
|
||||
source-2x-type="remote"
|
||||
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
|
||||
source-3x-type="remote"
|
||||
source-3x-uri="https://reactnative.dev/img/large_logo.svg"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style', () => {
|
||||
it('can be set', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<Image
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
resizeMode: 'contain',
|
||||
}}
|
||||
source={LOGO_SOURCE}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput().toJSX()).toEqual(
|
||||
<rn-image
|
||||
height="100.000000"
|
||||
overflow="hidden"
|
||||
resizeMode="contain"
|
||||
width="100.000000"
|
||||
source-scale="1"
|
||||
source-type="remote"
|
||||
source-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testID', () => {
|
||||
it('can be set', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image testID="test" source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['testID']}).toJSX()).toEqual(
|
||||
<rn-image testID="test" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tintColor', () => {
|
||||
it('can be set', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image tintColor="red" source={LOGO_SOURCE} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['tintColor']}).toJSX()).toEqual(
|
||||
<rn-image tintColor="rgba(255, 0, 0, 1)" />,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ref', () => {
|
||||
describe('instance', () => {
|
||||
it('is an element node', () => {
|
||||
const elementRef = createRef<HostInstance>();
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image ref={elementRef} />);
|
||||
});
|
||||
|
||||
expect(elementRef.current).toBeInstanceOf(ReactNativeElement);
|
||||
});
|
||||
|
||||
it('uses the "RN:Image" tag name', () => {
|
||||
const elementRef = createRef<HostInstance>();
|
||||
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<Image ref={elementRef} />);
|
||||
});
|
||||
|
||||
const element = ensureInstance(elementRef.current, ReactNativeElement);
|
||||
expect(element.tagName).toBe('RN:Image');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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 type {HostInstance} from 'react-native';
|
||||
|
||||
import * as Fantom from '@react-native/fantom';
|
||||
import * as React from 'react';
|
||||
import {createRef} from 'react';
|
||||
import {ImageBackground} from 'react-native';
|
||||
import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureInstance';
|
||||
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
|
||||
|
||||
describe('<ImageBackground>', () => {
|
||||
describe('props', () => {
|
||||
describe('ImageProps', () => {
|
||||
it('can have local source', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<ImageBackground source={require('./img/img1.png')} />);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-scale="1"
|
||||
source-size="{1, 1}"
|
||||
source-type="local"
|
||||
source-uri="file://drawable-mdpi/packages_reactnative_libraries_image___tests___img_img1.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can have remote source', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<ImageBackground
|
||||
source={{
|
||||
uri: 'https://reactnative.dev/img/tiny_logo.png',
|
||||
width: 100,
|
||||
height: 100,
|
||||
scale: 2,
|
||||
cache: 'only-if-cached',
|
||||
method: 'POST',
|
||||
body: 'name=React+Native',
|
||||
headers: {
|
||||
Authorization: 'Basic RandomString',
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-body="name=React+Native"
|
||||
source-cache="only-if-cached"
|
||||
source-header-Authorization="Basic RandomString"
|
||||
source-method="POST"
|
||||
source-scale="2"
|
||||
source-size="{100, 100}"
|
||||
source-type="remote"
|
||||
source-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it('can have srcSet', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<ImageBackground srcSet="https://reactnative.dev/img/tiny_logo.png 1x, https://reactnative.dev/img/header_logo.svg 2x" />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(root.getRenderedOutput({props: ['source']}).toJSX()).toEqual(
|
||||
<rn-image
|
||||
source-1x-scale="1"
|
||||
source-1x-type="remote"
|
||||
source-1x-uri="https://reactnative.dev/img/tiny_logo.png"
|
||||
source-2x-scale="2"
|
||||
source-2x-type="remote"
|
||||
source-2x-uri="https://reactnative.dev/img/header_logo.svg"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style', () => {
|
||||
it('can be set', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<ImageBackground style={{width: 100, height: 100}} />);
|
||||
});
|
||||
|
||||
expect(
|
||||
root.getRenderedOutput({props: ['width|height']}).toJSX(),
|
||||
).toEqual(<rn-image width="100.000000" height="100.000000" />);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ref', () => {
|
||||
it('Allows to set a reference to the inner `Image` component', () => {
|
||||
const elementRef = createRef<HostInstance>();
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<ImageBackground imageRef={elementRef} />);
|
||||
});
|
||||
|
||||
const image = ensureInstance(elementRef.current, ReactNativeElement);
|
||||
expect(image.tagName).toBe('RN:Image');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -164,7 +164,7 @@ const InteractionManagerStub = {
|
||||
*/
|
||||
addListener(
|
||||
eventType: string,
|
||||
// $FlowIgnore[unclear-type]
|
||||
// $FlowFixMe[unclear-type]
|
||||
listener: (...args: any) => mixed,
|
||||
context: mixed,
|
||||
): EventSubscription {
|
||||
|
||||
@@ -43,7 +43,7 @@ Pod::Spec.new do |s|
|
||||
s.dependency "RCTTypeSafety"
|
||||
s.dependency "React-jsi"
|
||||
s.dependency "React-Core/RCTNetworkHeaders"
|
||||
|
||||
add_dependency(s, "React-debug")
|
||||
add_dependency(s, "React-RCTFBReactNativeSpec")
|
||||
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
|
||||
add_dependency(s, "React-featureflags")
|
||||
|
||||
@@ -26,8 +26,6 @@ import createPerformanceLogger from '../Utilities/createPerformanceLogger';
|
||||
import SceneTracker from '../Utilities/SceneTracker';
|
||||
import {coerceDisplayMode} from './DisplayMode';
|
||||
import HeadlessJsTaskError from './HeadlessJsTaskError';
|
||||
import NativeHeadlessJsTaskSupport from './NativeHeadlessJsTaskSupport';
|
||||
import renderApplication from './renderApplication';
|
||||
import {unmountComponentAtNodeAndRemoveContainer} from './RendererProxy';
|
||||
import invariant from 'invariant';
|
||||
|
||||
@@ -86,6 +84,7 @@ export function registerComponent(
|
||||
): string {
|
||||
const scopedPerformanceLogger = createPerformanceLogger();
|
||||
runnables[appKey] = (appParameters, displayMode) => {
|
||||
const renderApplication = require('./renderApplication').default;
|
||||
renderApplication(
|
||||
componentProviderInstrumentationHook(
|
||||
componentProvider,
|
||||
@@ -258,6 +257,9 @@ export function startHeadlessTask(
|
||||
taskKey: string,
|
||||
data: any,
|
||||
): void {
|
||||
const NativeHeadlessJsTaskSupport =
|
||||
require('./NativeHeadlessJsTaskSupport').default;
|
||||
|
||||
const taskProvider = taskProviders.get(taskKey);
|
||||
if (!taskProvider) {
|
||||
console.warn(`No task registered for key ${taskKey}`);
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
* @noformat
|
||||
* @nolint
|
||||
* @flow
|
||||
* @generated SignedSource<<16b364e89f43b8a47832b0dfb98af11e>>
|
||||
*
|
||||
* This file was sync'd from the facebook/react repository.
|
||||
* @generated SignedSource<<cf323fc5ca893bab5669c7d321660412>>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
* @noformat
|
||||
* @nolint
|
||||
* @flow strict-local
|
||||
* @generated SignedSource<<1dd9e9c3f20e37ae14e485fc6ee3d9e9>>
|
||||
*
|
||||
* This file was sync'd from the facebook/react repository.
|
||||
* @generated SignedSource<<908f5fb85384725318e261f40e49d9a6>>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
* @noformat
|
||||
* @nolint
|
||||
* @flow
|
||||
* @generated SignedSource<<e2c46705ed927302dbe9332dafba459d>>
|
||||
*
|
||||
* This file was sync'd from the facebook/react repository.
|
||||
* @generated SignedSource<<8f46fdc9267fcc4fdc9e76842fe24066>>
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
+1
-3
@@ -7,9 +7,7 @@
|
||||
* @noformat
|
||||
* @nolint
|
||||
* @flow strict-local
|
||||
* @generated SignedSource<<e8dce0e82b831c91465d04b49fb48ab2>>
|
||||
*
|
||||
* This file was sync'd from the facebook/react repository.
|
||||
* @generated SignedSource<<83073425aa3f71ced2c8c51f25a25938>>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
+1
-3
@@ -7,9 +7,7 @@
|
||||
* @noformat
|
||||
* @nolint
|
||||
* @flow strict-local
|
||||
* @generated SignedSource<<556d1487de0b9e4a09cbc67dd130a884>>
|
||||
*
|
||||
* This file was sync'd from the facebook/react repository.
|
||||
* @generated SignedSource<<52163887de05f1cff05388145cf85b3b>>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -18,9 +18,9 @@ export default function splitLayoutProps(props: ?____ViewStyle_Internal): {
|
||||
let inner: ?____ViewStyle_Internal = null;
|
||||
|
||||
if (props != null) {
|
||||
// $FlowIgnore[incompatible-exact] Will contain a subset of keys from `props`.
|
||||
// $FlowFixMe[incompatible-exact] Will contain a subset of keys from `props`.
|
||||
outer = {};
|
||||
// $FlowIgnore[incompatible-exact] Will contain a subset of keys from `props`.
|
||||
// $FlowFixMe[incompatible-exact] Will contain a subset of keys from `props`.
|
||||
inner = {};
|
||||
|
||||
for (const prop of Object.keys(props)) {
|
||||
|
||||
+517
-243
@@ -14,6 +14,7 @@ import type {GestureResponderEvent} from '../Types/CoreEventTypes';
|
||||
import type {NativeTextProps} from './TextNativeComponent';
|
||||
import type {PressRetentionOffset, TextProps} from './TextProps';
|
||||
|
||||
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
|
||||
import * as PressabilityDebug from '../Pressability/PressabilityDebug';
|
||||
import usePressability from '../Pressability/usePressability';
|
||||
import flattenStyle from '../StyleSheet/flattenStyle';
|
||||
@@ -35,156 +36,495 @@ type TextForwardRef = React.ElementRef<
|
||||
*
|
||||
* @see https://reactnative.dev/docs/text
|
||||
*/
|
||||
const TextImpl: component(
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...props: TextProps
|
||||
) = ({
|
||||
ref: forwardedRef,
|
||||
accessible,
|
||||
accessibilityLabel,
|
||||
accessibilityState,
|
||||
allowFontScaling,
|
||||
'aria-busy': ariaBusy,
|
||||
'aria-checked': ariaChecked,
|
||||
'aria-disabled': ariaDisabled,
|
||||
'aria-expanded': ariaExpanded,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-selected': ariaSelected,
|
||||
children,
|
||||
ellipsizeMode,
|
||||
disabled,
|
||||
id,
|
||||
nativeID,
|
||||
numberOfLines,
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
selectable,
|
||||
selectionColor,
|
||||
suppressHighlighting,
|
||||
style,
|
||||
...restProps
|
||||
}: {
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...TextProps,
|
||||
}) => {
|
||||
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
|
||||
|
||||
let _accessibilityState: ?TextProps['accessibilityState'] =
|
||||
accessibilityState;
|
||||
if (
|
||||
ariaBusy != null ||
|
||||
ariaChecked != null ||
|
||||
ariaDisabled != null ||
|
||||
ariaExpanded != null ||
|
||||
ariaSelected != null
|
||||
) {
|
||||
if (_accessibilityState != null) {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy ?? _accessibilityState.busy,
|
||||
checked: ariaChecked ?? _accessibilityState.checked,
|
||||
disabled: ariaDisabled ?? _accessibilityState.disabled,
|
||||
expanded: ariaExpanded ?? _accessibilityState.expanded,
|
||||
selected: ariaSelected ?? _accessibilityState.selected,
|
||||
};
|
||||
} else {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy,
|
||||
checked: ariaChecked,
|
||||
disabled: ariaDisabled,
|
||||
expanded: ariaExpanded,
|
||||
selected: ariaSelected,
|
||||
};
|
||||
let _TextImpl;
|
||||
if (ReactNativeFeatureFlags.reduceDefaultPropsInText()) {
|
||||
const TextImplNoDefaultProps: component(
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...props: TextProps
|
||||
) = ({
|
||||
ref: forwardedRef,
|
||||
accessible,
|
||||
accessibilityLabel,
|
||||
accessibilityState,
|
||||
allowFontScaling,
|
||||
'aria-busy': ariaBusy,
|
||||
'aria-checked': ariaChecked,
|
||||
'aria-disabled': ariaDisabled,
|
||||
'aria-expanded': ariaExpanded,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-selected': ariaSelected,
|
||||
children,
|
||||
ellipsizeMode,
|
||||
disabled,
|
||||
id,
|
||||
nativeID,
|
||||
numberOfLines,
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
selectable,
|
||||
selectionColor,
|
||||
suppressHighlighting,
|
||||
style,
|
||||
...restProps
|
||||
}: {
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...TextProps,
|
||||
}) => {
|
||||
const processedProps = restProps as {
|
||||
...NativeTextProps,
|
||||
};
|
||||
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
|
||||
let _accessibilityState: ?TextProps['accessibilityState'] =
|
||||
accessibilityState;
|
||||
if (
|
||||
ariaBusy != null ||
|
||||
ariaChecked != null ||
|
||||
ariaDisabled != null ||
|
||||
ariaExpanded != null ||
|
||||
ariaSelected != null
|
||||
) {
|
||||
if (_accessibilityState != null) {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy ?? _accessibilityState.busy,
|
||||
checked: ariaChecked ?? _accessibilityState.checked,
|
||||
disabled: ariaDisabled ?? _accessibilityState.disabled,
|
||||
expanded: ariaExpanded ?? _accessibilityState.expanded,
|
||||
selected: ariaSelected ?? _accessibilityState.selected,
|
||||
};
|
||||
} else {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy,
|
||||
checked: ariaChecked,
|
||||
disabled: ariaDisabled,
|
||||
expanded: ariaExpanded,
|
||||
selected: ariaSelected,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _accessibilityStateDisabled = _accessibilityState?.disabled;
|
||||
const _disabled = disabled ?? _accessibilityStateDisabled;
|
||||
const _accessibilityStateDisabled = _accessibilityState?.disabled;
|
||||
const _disabled = disabled ?? _accessibilityStateDisabled;
|
||||
|
||||
const isPressable =
|
||||
(onPress != null ||
|
||||
onLongPress != null ||
|
||||
onStartShouldSetResponder != null) &&
|
||||
_disabled !== true;
|
||||
|
||||
// TODO: Move this processing to the view configuration.
|
||||
const _selectionColor =
|
||||
selectionColor != null ? processColor(selectionColor) : undefined;
|
||||
|
||||
let _style = style;
|
||||
if (__DEV__) {
|
||||
if (PressabilityDebug.isEnabled() && onPress != null) {
|
||||
_style = [style, {color: 'magenta'}];
|
||||
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
|
||||
// falsy states we need to update the accessibilityState object to use the disabled prop.
|
||||
if (
|
||||
_accessibilityState != null &&
|
||||
_disabled !== _accessibilityStateDisabled &&
|
||||
((_disabled != null && _disabled !== false) ||
|
||||
(_accessibilityStateDisabled != null &&
|
||||
_accessibilityStateDisabled !== false))
|
||||
) {
|
||||
_accessibilityState.disabled = _disabled;
|
||||
}
|
||||
}
|
||||
|
||||
let _numberOfLines = numberOfLines;
|
||||
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
|
||||
const _accessible = Platform.select({
|
||||
ios: accessible !== false,
|
||||
android:
|
||||
accessible == null
|
||||
? onPress != null || onLongPress != null
|
||||
: accessible,
|
||||
default: accessible,
|
||||
});
|
||||
|
||||
const isPressable =
|
||||
(onPress != null ||
|
||||
onLongPress != null ||
|
||||
onStartShouldSetResponder != null) &&
|
||||
_disabled !== true;
|
||||
|
||||
// TODO: Move this processing to the view configuration.
|
||||
const _selectionColor =
|
||||
selectionColor != null ? processColor(selectionColor) : undefined;
|
||||
|
||||
let _style = style;
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
|
||||
if (PressabilityDebug.isEnabled() && onPress != null) {
|
||||
_style = [style, {color: 'magenta'}];
|
||||
}
|
||||
}
|
||||
|
||||
let _numberOfLines = numberOfLines;
|
||||
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
|
||||
);
|
||||
}
|
||||
_numberOfLines = 0;
|
||||
}
|
||||
|
||||
let _selectable = selectable;
|
||||
|
||||
let processedStyle = flattenStyle<TextStyleProp>(_style);
|
||||
if (processedStyle != null) {
|
||||
let overrides: ?{...TextStyleInternal} = null;
|
||||
if (typeof processedStyle.fontWeight === 'number') {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.fontWeight =
|
||||
// $FlowFixMe[incompatible-cast]
|
||||
(String(processedStyle.fontWeight): TextStyleInternal['fontWeight']);
|
||||
}
|
||||
|
||||
if (processedStyle.userSelect != null) {
|
||||
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.userSelect = undefined;
|
||||
}
|
||||
|
||||
if (processedStyle.verticalAlign != null) {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.textAlignVertical =
|
||||
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
|
||||
overrides.verticalAlign = undefined;
|
||||
}
|
||||
|
||||
if (overrides != null) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
_style = [_style, overrides];
|
||||
}
|
||||
}
|
||||
|
||||
const _nativeID = id ?? nativeID;
|
||||
|
||||
if (_accessibilityLabel !== undefined) {
|
||||
processedProps.accessibilityLabel = _accessibilityLabel;
|
||||
}
|
||||
if (_accessibilityState !== undefined) {
|
||||
processedProps.accessibilityState = _accessibilityState;
|
||||
}
|
||||
if (_nativeID !== undefined) {
|
||||
processedProps.nativeID = _nativeID;
|
||||
}
|
||||
if (_numberOfLines !== undefined) {
|
||||
processedProps.numberOfLines = _numberOfLines;
|
||||
}
|
||||
if (_selectable !== undefined) {
|
||||
processedProps.selectable = _selectable;
|
||||
}
|
||||
if (_style !== undefined) {
|
||||
processedProps.style = _style;
|
||||
}
|
||||
if (_selectionColor !== undefined) {
|
||||
processedProps.selectionColor = _selectionColor;
|
||||
}
|
||||
|
||||
let textPressabilityProps: ?TextPressabilityProps;
|
||||
if (isPressable) {
|
||||
textPressabilityProps = {
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
suppressHighlighting,
|
||||
};
|
||||
}
|
||||
|
||||
const hasTextAncestor = useContext(TextAncestorContext);
|
||||
if (hasTextAncestor) {
|
||||
processedProps.disabled = disabled;
|
||||
processedProps.children = children;
|
||||
if (isPressable) {
|
||||
return (
|
||||
<NativePressableVirtualText
|
||||
ref={forwardedRef}
|
||||
textProps={processedProps}
|
||||
textPressabilityProps={textPressabilityProps ?? {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <NativeVirtualText {...processedProps} ref={forwardedRef} />;
|
||||
}
|
||||
|
||||
let nativeText = null;
|
||||
|
||||
processedProps.accessible = _accessible;
|
||||
processedProps.allowFontScaling = allowFontScaling !== false;
|
||||
processedProps.disabled = _disabled;
|
||||
processedProps.ellipsizeMode = ellipsizeMode ?? 'tail';
|
||||
processedProps.children = children;
|
||||
|
||||
if (isPressable) {
|
||||
nativeText = (
|
||||
<NativePressableText
|
||||
ref={forwardedRef}
|
||||
textProps={processedProps}
|
||||
textPressabilityProps={textPressabilityProps ?? {}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
nativeText = <NativeText {...processedProps} ref={forwardedRef} />;
|
||||
}
|
||||
|
||||
if (children == null) {
|
||||
return nativeText;
|
||||
}
|
||||
|
||||
// If the children do not contain a JSX element it would not be possible to have a
|
||||
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
|
||||
// which has a performance overhead. Since we do this for performance reasons we need
|
||||
// to keep the check simple to avoid regressing overall perf. For this reason the
|
||||
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
|
||||
// to capture the majority of `Text` uses but also not make this check too expensive.
|
||||
if (Array.isArray(children) && children.length <= 3) {
|
||||
let hasNonTextChild = false;
|
||||
for (let child of children) {
|
||||
if (child != null && typeof child === 'object') {
|
||||
hasNonTextChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasNonTextChild) {
|
||||
return nativeText;
|
||||
}
|
||||
} else if (typeof children !== 'object') {
|
||||
return nativeText;
|
||||
}
|
||||
|
||||
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
|
||||
};
|
||||
_TextImpl = TextImplNoDefaultProps;
|
||||
} else {
|
||||
const TextImplLegacy: component(
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...props: TextProps
|
||||
) = ({
|
||||
ref: forwardedRef,
|
||||
accessible,
|
||||
accessibilityLabel,
|
||||
accessibilityState,
|
||||
allowFontScaling,
|
||||
'aria-busy': ariaBusy,
|
||||
'aria-checked': ariaChecked,
|
||||
'aria-disabled': ariaDisabled,
|
||||
'aria-expanded': ariaExpanded,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-selected': ariaSelected,
|
||||
children,
|
||||
ellipsizeMode,
|
||||
disabled,
|
||||
id,
|
||||
nativeID,
|
||||
numberOfLines,
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
selectable,
|
||||
selectionColor,
|
||||
suppressHighlighting,
|
||||
style,
|
||||
...restProps
|
||||
}: {
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...TextProps,
|
||||
}) => {
|
||||
const _accessibilityLabel = ariaLabel ?? accessibilityLabel;
|
||||
|
||||
let _accessibilityState: ?TextProps['accessibilityState'] =
|
||||
accessibilityState;
|
||||
if (
|
||||
ariaBusy != null ||
|
||||
ariaChecked != null ||
|
||||
ariaDisabled != null ||
|
||||
ariaExpanded != null ||
|
||||
ariaSelected != null
|
||||
) {
|
||||
if (_accessibilityState != null) {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy ?? _accessibilityState.busy,
|
||||
checked: ariaChecked ?? _accessibilityState.checked,
|
||||
disabled: ariaDisabled ?? _accessibilityState.disabled,
|
||||
expanded: ariaExpanded ?? _accessibilityState.expanded,
|
||||
selected: ariaSelected ?? _accessibilityState.selected,
|
||||
};
|
||||
} else {
|
||||
_accessibilityState = {
|
||||
busy: ariaBusy,
|
||||
checked: ariaChecked,
|
||||
disabled: ariaDisabled,
|
||||
expanded: ariaExpanded,
|
||||
selected: ariaSelected,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const _accessibilityStateDisabled = _accessibilityState?.disabled;
|
||||
const _disabled = disabled ?? _accessibilityStateDisabled;
|
||||
|
||||
const isPressable =
|
||||
(onPress != null ||
|
||||
onLongPress != null ||
|
||||
onStartShouldSetResponder != null) &&
|
||||
_disabled !== true;
|
||||
|
||||
// TODO: Move this processing to the view configuration.
|
||||
const _selectionColor =
|
||||
selectionColor != null ? processColor(selectionColor) : undefined;
|
||||
|
||||
let _style = style;
|
||||
if (__DEV__) {
|
||||
if (PressabilityDebug.isEnabled() && onPress != null) {
|
||||
_style = [style, {color: 'magenta'}];
|
||||
}
|
||||
}
|
||||
|
||||
let _numberOfLines = numberOfLines;
|
||||
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`'numberOfLines' in <Text> must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`,
|
||||
);
|
||||
}
|
||||
_numberOfLines = 0;
|
||||
}
|
||||
|
||||
let _selectable = selectable;
|
||||
|
||||
let processedStyle = flattenStyle<TextStyleProp>(_style);
|
||||
if (processedStyle != null) {
|
||||
let overrides: ?{...TextStyleInternal} = null;
|
||||
if (typeof processedStyle.fontWeight === 'number') {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.fontWeight =
|
||||
// $FlowFixMe[incompatible-cast]
|
||||
(processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
|
||||
}
|
||||
|
||||
if (processedStyle.userSelect != null) {
|
||||
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.userSelect = undefined;
|
||||
}
|
||||
|
||||
if (processedStyle.verticalAlign != null) {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.textAlignVertical =
|
||||
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
|
||||
overrides.verticalAlign = undefined;
|
||||
}
|
||||
|
||||
if (overrides != null) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
_style = [_style, overrides];
|
||||
}
|
||||
}
|
||||
|
||||
const _nativeID = id ?? nativeID;
|
||||
|
||||
const hasTextAncestor = useContext(TextAncestorContext);
|
||||
if (hasTextAncestor) {
|
||||
if (isPressable) {
|
||||
return (
|
||||
<NativePressableVirtualText
|
||||
ref={forwardedRef}
|
||||
textProps={{
|
||||
...restProps,
|
||||
accessibilityLabel: _accessibilityLabel,
|
||||
accessibilityState: _accessibilityState,
|
||||
nativeID: _nativeID,
|
||||
numberOfLines: _numberOfLines,
|
||||
selectable: _selectable,
|
||||
selectionColor: _selectionColor,
|
||||
style: _style,
|
||||
disabled: disabled,
|
||||
children,
|
||||
}}
|
||||
textPressabilityProps={{
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
suppressHighlighting,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NativeVirtualText
|
||||
{...restProps}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityState={_accessibilityState}
|
||||
nativeID={_nativeID}
|
||||
numberOfLines={_numberOfLines}
|
||||
ref={forwardedRef}
|
||||
selectable={_selectable}
|
||||
selectionColor={_selectionColor}
|
||||
style={_style}
|
||||
disabled={disabled}>
|
||||
{children}
|
||||
</NativeVirtualText>
|
||||
);
|
||||
}
|
||||
_numberOfLines = 0;
|
||||
}
|
||||
|
||||
let _selectable = selectable;
|
||||
|
||||
let processedStyle = flattenStyle<TextStyleProp>(_style);
|
||||
if (processedStyle != null) {
|
||||
let overrides: ?{...TextStyleInternal} = null;
|
||||
if (typeof processedStyle.fontWeight === 'number') {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.fontWeight =
|
||||
// $FlowFixMe[incompatible-cast]
|
||||
(processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
|
||||
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
|
||||
// falsy states we need to update the accessibilityState object to use the disabled prop.
|
||||
if (
|
||||
_disabled !== _accessibilityStateDisabled &&
|
||||
((_disabled != null && _disabled !== false) ||
|
||||
(_accessibilityStateDisabled != null &&
|
||||
_accessibilityStateDisabled !== false))
|
||||
) {
|
||||
_accessibilityState = {..._accessibilityState, disabled: _disabled};
|
||||
}
|
||||
|
||||
if (processedStyle.userSelect != null) {
|
||||
_selectable = userSelectToSelectableMap[processedStyle.userSelect];
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.userSelect = undefined;
|
||||
}
|
||||
const _accessible = Platform.select({
|
||||
ios: accessible !== false,
|
||||
android:
|
||||
accessible == null
|
||||
? onPress != null || onLongPress != null
|
||||
: accessible,
|
||||
default: accessible,
|
||||
});
|
||||
|
||||
if (processedStyle.verticalAlign != null) {
|
||||
overrides = overrides || ({}: {...TextStyleInternal});
|
||||
overrides.textAlignVertical =
|
||||
verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign];
|
||||
overrides.verticalAlign = undefined;
|
||||
}
|
||||
|
||||
if (overrides != null) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
_style = [_style, overrides];
|
||||
}
|
||||
}
|
||||
|
||||
const _nativeID = id ?? nativeID;
|
||||
|
||||
const hasTextAncestor = useContext(TextAncestorContext);
|
||||
if (hasTextAncestor) {
|
||||
let nativeText = null;
|
||||
if (isPressable) {
|
||||
return (
|
||||
<NativePressableVirtualText
|
||||
nativeText = (
|
||||
<NativePressableText
|
||||
ref={forwardedRef}
|
||||
textProps={{
|
||||
...restProps,
|
||||
accessibilityLabel: _accessibilityLabel,
|
||||
accessibilityState: _accessibilityState,
|
||||
accessible: _accessible,
|
||||
allowFontScaling: allowFontScaling !== false,
|
||||
disabled: _disabled,
|
||||
ellipsizeMode: ellipsizeMode ?? 'tail',
|
||||
nativeID: _nativeID,
|
||||
numberOfLines: _numberOfLines,
|
||||
selectable: _selectable,
|
||||
selectionColor: _selectionColor,
|
||||
style: _style,
|
||||
disabled: disabled,
|
||||
children,
|
||||
}}
|
||||
textPressabilityProps={{
|
||||
@@ -203,127 +543,61 @@ const TextImpl: component(
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
nativeText = (
|
||||
<NativeText
|
||||
{...restProps}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessible={_accessible}
|
||||
allowFontScaling={allowFontScaling !== false}
|
||||
disabled={_disabled}
|
||||
ellipsizeMode={ellipsizeMode ?? 'tail'}
|
||||
nativeID={_nativeID}
|
||||
numberOfLines={_numberOfLines}
|
||||
ref={forwardedRef}
|
||||
selectable={_selectable}
|
||||
selectionColor={_selectionColor}
|
||||
style={_style}>
|
||||
{children}
|
||||
</NativeText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NativeVirtualText
|
||||
{...restProps}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityState={_accessibilityState}
|
||||
nativeID={_nativeID}
|
||||
numberOfLines={_numberOfLines}
|
||||
ref={forwardedRef}
|
||||
selectable={_selectable}
|
||||
selectionColor={_selectionColor}
|
||||
style={_style}
|
||||
disabled={disabled}>
|
||||
{children}
|
||||
</NativeVirtualText>
|
||||
);
|
||||
}
|
||||
|
||||
// If the disabled prop and accessibilityState.disabled are out of sync but not both in
|
||||
// falsy states we need to update the accessibilityState object to use the disabled prop.
|
||||
if (
|
||||
_disabled !== _accessibilityStateDisabled &&
|
||||
((_disabled != null && _disabled !== false) ||
|
||||
(_accessibilityStateDisabled != null &&
|
||||
_accessibilityStateDisabled !== false))
|
||||
) {
|
||||
_accessibilityState = {..._accessibilityState, disabled: _disabled};
|
||||
}
|
||||
|
||||
const _accessible = Platform.select({
|
||||
ios: accessible !== false,
|
||||
android:
|
||||
accessible == null ? onPress != null || onLongPress != null : accessible,
|
||||
default: accessible,
|
||||
});
|
||||
|
||||
let nativeText = null;
|
||||
if (isPressable) {
|
||||
nativeText = (
|
||||
<NativePressableText
|
||||
ref={forwardedRef}
|
||||
textProps={{
|
||||
...restProps,
|
||||
accessibilityLabel: _accessibilityLabel,
|
||||
accessibilityState: _accessibilityState,
|
||||
accessible: _accessible,
|
||||
allowFontScaling: allowFontScaling !== false,
|
||||
disabled: _disabled,
|
||||
ellipsizeMode: ellipsizeMode ?? 'tail',
|
||||
nativeID: _nativeID,
|
||||
numberOfLines: _numberOfLines,
|
||||
selectable: _selectable,
|
||||
selectionColor: _selectionColor,
|
||||
style: _style,
|
||||
children,
|
||||
}}
|
||||
textPressabilityProps={{
|
||||
onLongPress,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onStartShouldSetResponder,
|
||||
pressRetentionOffset,
|
||||
suppressHighlighting,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
nativeText = (
|
||||
<NativeText
|
||||
{...restProps}
|
||||
accessibilityLabel={_accessibilityLabel}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessible={_accessible}
|
||||
allowFontScaling={allowFontScaling !== false}
|
||||
disabled={_disabled}
|
||||
ellipsizeMode={ellipsizeMode ?? 'tail'}
|
||||
nativeID={_nativeID}
|
||||
numberOfLines={_numberOfLines}
|
||||
ref={forwardedRef}
|
||||
selectable={_selectable}
|
||||
selectionColor={_selectionColor}
|
||||
style={_style}>
|
||||
{children}
|
||||
</NativeText>
|
||||
);
|
||||
}
|
||||
|
||||
if (children == null) {
|
||||
return nativeText;
|
||||
}
|
||||
|
||||
// If the children do not contain a JSX element it would not be possible to have a
|
||||
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
|
||||
// which has a performance overhead. Since we do this for performance reasons we need
|
||||
// to keep the check simple to avoid regressing overall perf. For this reason the
|
||||
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
|
||||
// to capture the majority of `Text` uses but also not make this check too expensive.
|
||||
if (Array.isArray(children) && children.length <= 3) {
|
||||
let hasNonTextChild = false;
|
||||
for (let child of children) {
|
||||
if (child != null && typeof child === 'object') {
|
||||
hasNonTextChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasNonTextChild) {
|
||||
if (children == null) {
|
||||
return nativeText;
|
||||
}
|
||||
} else if (typeof children !== 'object') {
|
||||
return nativeText;
|
||||
}
|
||||
|
||||
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
|
||||
};
|
||||
// If the children do not contain a JSX element it would not be possible to have a
|
||||
// nested `Text` component so we can skip adding the `TextAncestorContext` context wrapper
|
||||
// which has a performance overhead. Since we do this for performance reasons we need
|
||||
// to keep the check simple to avoid regressing overall perf. For this reason the
|
||||
// `children.length` constant is set to `3`, this should be a reasonable tradeoff
|
||||
// to capture the majority of `Text` uses but also not make this check too expensive.
|
||||
if (Array.isArray(children) && children.length <= 3) {
|
||||
let hasNonTextChild = false;
|
||||
for (let child of children) {
|
||||
if (child != null && typeof child === 'object') {
|
||||
hasNonTextChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasNonTextChild) {
|
||||
return nativeText;
|
||||
}
|
||||
} else if (typeof children !== 'object') {
|
||||
return nativeText;
|
||||
}
|
||||
|
||||
return <TextAncestorContext value={true}>{nativeText}</TextAncestorContext>;
|
||||
};
|
||||
_TextImpl = TextImplLegacy;
|
||||
}
|
||||
|
||||
const TextImpl: component(
|
||||
ref?: React.RefSetter<TextForwardRef>,
|
||||
...props: TextProps
|
||||
) = _TextImpl;
|
||||
|
||||
TextImpl.displayName = 'Text';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @fantom_flags reduceDefaultPropsInText:*
|
||||
*/
|
||||
|
||||
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @fantom_flags reduceDefaultPropsInText:*
|
||||
*/
|
||||
|
||||
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
|
||||
|
||||
@@ -128,7 +128,6 @@ const HMRClient: HMRClientNativeInterface = {
|
||||
JSON.stringify({
|
||||
type: 'log',
|
||||
level,
|
||||
mode: global.RN$Bridgeless === true ? 'NOBRIDGE' : 'BRIDGE',
|
||||
data: data.map(item =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
|
||||
@@ -43,7 +43,7 @@ function TestComponent(
|
||||
}
|
||||
|
||||
function id(instance: HostInstance | null): string | null {
|
||||
// $FlowIgnore[prop-missing] - Intentional.
|
||||
// $FlowFixMe[prop-missing] - Intentional.
|
||||
return instance?.props?.id ?? null;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ test('accepts a ref object', () => {
|
||||
const ledger: Array<{[string]: string | null}> = [];
|
||||
|
||||
const ref = {
|
||||
// $FlowIgnore[unsafe-getters-setters] - Intentional.
|
||||
// $FlowFixMe[unsafe-getters-setters] - Intentional.
|
||||
set current(current: HostInstance | null) {
|
||||
ledger.push({ref: id(current)});
|
||||
},
|
||||
@@ -140,7 +140,7 @@ test('invokes refs in order', () => {
|
||||
ledger.push({refA: id(current)});
|
||||
};
|
||||
const refB = {
|
||||
// $FlowIgnore[unsafe-getters-setters] - Intentional.
|
||||
// $FlowFixMe[unsafe-getters-setters] - Intentional.
|
||||
set current(current: HostInstance | null) {
|
||||
ledger.push({refB: id(current)});
|
||||
},
|
||||
@@ -149,7 +149,7 @@ test('invokes refs in order', () => {
|
||||
ledger.push({refC: id(current)});
|
||||
};
|
||||
const refD = {
|
||||
// $FlowIgnore[unsafe-getters-setters] - Intentional.
|
||||
// $FlowFixMe[unsafe-getters-setters] - Intentional.
|
||||
set current(current: HostInstance | null) {
|
||||
ledger.push({refD: id(current)});
|
||||
},
|
||||
|
||||
@@ -141,8 +141,7 @@ NSMutableArray<NSString *> *getModulesLoadedWithOldArch(void)
|
||||
void RCTRegisterModule(Class);
|
||||
void RCTRegisterModule(Class moduleClass)
|
||||
{
|
||||
if (RCTAreLegacyLogsEnabled() && RCTIsNewArchEnabled() &&
|
||||
![getCoreModuleClasses() containsObject:[moduleClass description]]) {
|
||||
if (RCTAreLegacyLogsEnabled() && ![getCoreModuleClasses() containsObject:[moduleClass description]]) {
|
||||
addModuleLoadedWithOldArch([moduleClass description]);
|
||||
}
|
||||
static dispatch_once_t onceToken;
|
||||
@@ -183,7 +182,7 @@ NSString *RCTBridgeModuleNameForClass(Class cls)
|
||||
return RCTDropReactPrefixes(name);
|
||||
}
|
||||
|
||||
static BOOL turboModuleEnabled = NO;
|
||||
static const BOOL turboModuleEnabled = YES;
|
||||
BOOL RCTTurboModuleEnabled(void)
|
||||
{
|
||||
#if RCT_DEBUG
|
||||
@@ -197,7 +196,7 @@ BOOL RCTTurboModuleEnabled(void)
|
||||
|
||||
void RCTEnableTurboModule(BOOL enabled)
|
||||
{
|
||||
turboModuleEnabled = enabled;
|
||||
// The new Architecture is enabled by default and we are ignoring changes to the TurboModule system.
|
||||
}
|
||||
|
||||
static BOOL turboModuleInteropEnabled = NO;
|
||||
|
||||
@@ -43,8 +43,7 @@ UIDeviceOrientation RCTDeviceOrientation(void);
|
||||
// Whether the New Architecture is enabled or not
|
||||
BOOL RCTIsNewArchEnabled(void)
|
||||
{
|
||||
NSNumber *rctNewArchEnabled = (NSNumber *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTNewArchEnabled"];
|
||||
return rctNewArchEnabled == nil || rctNewArchEnabled.boolValue;
|
||||
return YES;
|
||||
}
|
||||
void RCTSetNewArchEnabled(BOOL enabled)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ Pod::Spec.new do |s|
|
||||
s.dependency "React-RCTImage", version
|
||||
s.dependency "React-jsi", version
|
||||
s.dependency 'React-RCTBlob'
|
||||
|
||||
add_dependency(s, "React-debug")
|
||||
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
|
||||
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
|
||||
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
|
||||
|
||||
+22
@@ -96,6 +96,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
|
||||
|
||||
@interface RCTModalHostViewComponentView () <RCTFabricModalHostViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, weak) UIView *accessibilityFocusedView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTModalHostViewComponentView {
|
||||
@@ -148,6 +150,7 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
|
||||
{
|
||||
BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window;
|
||||
if (shouldBePresented) {
|
||||
[self saveAccessibilityFocusedView];
|
||||
self.viewController.presentationController.delegate = self;
|
||||
|
||||
_isPresented = YES;
|
||||
@@ -179,6 +182,8 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
|
||||
if (eventEmitter) {
|
||||
eventEmitter->onDismiss(ModalHostViewEventEmitter::OnDismiss{});
|
||||
}
|
||||
|
||||
[self restoreAccessibilityFocusedView];
|
||||
}];
|
||||
}
|
||||
}
|
||||
@@ -207,6 +212,23 @@ static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(
|
||||
[self ensurePresentedOnlyIfNeeded];
|
||||
}
|
||||
|
||||
- (void)saveAccessibilityFocusedView
|
||||
{
|
||||
id focusedElement = UIAccessibilityFocusedElement(nil);
|
||||
if (focusedElement && [focusedElement isKindOfClass:[UIView class]]) {
|
||||
self.accessibilityFocusedView = (UIView *)focusedElement;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)restoreAccessibilityFocusedView
|
||||
{
|
||||
id viewToFocus = self.accessibilityFocusedView;
|
||||
if (viewToFocus) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, viewToFocus);
|
||||
self.accessibilityFocusedView = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - RCTFabricModalHostViewControllerDelegate
|
||||
|
||||
- (void)boundsDidChange:(CGRect)newBounds
|
||||
|
||||
@@ -92,6 +92,7 @@ Pod::Spec.new do |s|
|
||||
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
|
||||
add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork')
|
||||
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
|
||||
add_dependency(s, "React-performancecdpmetrics", :framework_name => 'React_performancecdpmetrics')
|
||||
add_dependency(s, "React-renderercss")
|
||||
add_dependency(s, "React-RCTFBReactNativeSpec")
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.dependency "React-Core"
|
||||
s.dependency "React-jsi"
|
||||
add_dependency(s, "React-debug")
|
||||
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
|
||||
add_dependency(s, "React-jsitooling", :framework_name => "JSITooling")
|
||||
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
|
||||
|
||||
@@ -1140,11 +1140,6 @@ public abstract class com/facebook/react/bridge/ReactContextBaseJavaModule : com
|
||||
protected final fun getCurrentActivity ()Landroid/app/Activity;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/ReactCxxErrorHandler {
|
||||
public static final field INSTANCE Lcom/facebook/react/bridge/ReactCxxErrorHandler;
|
||||
public static final fun setHandleErrorFunc (Ljava/lang/Object;Ljava/lang/reflect/Method;)V
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/ReactMarker {
|
||||
public static final field INSTANCE Lcom/facebook/react/bridge/ReactMarker;
|
||||
public static final fun addFabricListener (Lcom/facebook/react/bridge/ReactMarker$FabricMarkerListener;)V
|
||||
@@ -1309,12 +1304,6 @@ public abstract interface annotation class com/facebook/react/bridge/ReactMethod
|
||||
public abstract interface class com/facebook/react/bridge/ReactModuleWithSpec {
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/ReactNoCrashBridgeNotAllowedSoftException : com/facebook/react/bridge/ReactNoCrashSoftException {
|
||||
public fun <init> (Ljava/lang/String;)V
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun <init> (Ljava/lang/Throwable;)V
|
||||
}
|
||||
|
||||
public class com/facebook/react/bridge/ReactNoCrashSoftException : java/lang/RuntimeException {
|
||||
public fun <init> (Ljava/lang/String;)V
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
@@ -3359,8 +3348,10 @@ public final class com/facebook/react/uimanager/DisplayMetricsHolder {
|
||||
public static final fun getDisplayMetricsWritableMap (D)Lcom/facebook/react/bridge/WritableMap;
|
||||
public static final fun getScreenDisplayMetrics ()Landroid/util/DisplayMetrics;
|
||||
public static final fun getWindowDisplayMetrics ()Landroid/util/DisplayMetrics;
|
||||
public static final fun initDisplayMetrics (Landroid/content/Context;)V
|
||||
public static final fun initDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
|
||||
public static final fun initScreenDisplayMetrics (Landroid/content/Context;)V
|
||||
public static final fun initScreenDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
|
||||
public static final fun initWindowDisplayMetrics (Landroid/content/Context;)V
|
||||
public static final fun initWindowDisplayMetricsIfNotInitialized (Landroid/content/Context;)V
|
||||
public static final fun setScreenDisplayMetrics (Landroid/util/DisplayMetrics;)V
|
||||
public static final fun setWindowDisplayMetrics (Landroid/util/DisplayMetrics;)V
|
||||
}
|
||||
|
||||
@@ -630,6 +630,7 @@ dependencies {
|
||||
api(libs.androidx.autofill)
|
||||
api(libs.androidx.swiperefreshlayout)
|
||||
api(libs.androidx.tracing)
|
||||
api(libs.androidx.window)
|
||||
|
||||
api(libs.fbjni)
|
||||
api(libs.fresco)
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ public class MemoryPressureRouter(context: Context) : ComponentCallbacks2 {
|
||||
context.applicationContext.registerComponentCallbacks(this)
|
||||
}
|
||||
|
||||
public fun destroy(context: Context): Unit {
|
||||
public fun destroy(context: Context) {
|
||||
context.applicationContext.unregisterComponentCallbacks(this)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
|
||||
/** Responsible for dispatching events specific for hardware inputs. */
|
||||
internal class ReactAndroidHWInputDeviceHelper() {
|
||||
internal class ReactAndroidHWInputDeviceHelper {
|
||||
/**
|
||||
* We keep a reference to the last focused view id so that we can send it as a target for key
|
||||
* events and be able to send a blur event when focus changes.
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ public open class ReactFragment : Fragment(), PermissionAwareActivity {
|
||||
permissions: Array<String>,
|
||||
requestCode: Int,
|
||||
listener: PermissionListener?
|
||||
): Unit {
|
||||
) {
|
||||
permissionListener = listener
|
||||
requestPermissions(permissions, requestCode)
|
||||
}
|
||||
|
||||
+15
-1
@@ -229,6 +229,9 @@ public class ReactInstanceManager {
|
||||
return new ReactInstanceManagerBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection deprecation
|
||||
*/
|
||||
/* package */ ReactInstanceManager(
|
||||
Context applicationContext,
|
||||
@Nullable Activity currentActivity,
|
||||
@@ -259,7 +262,11 @@ public class ReactInstanceManager {
|
||||
FLog.d(TAG, "ReactInstanceManager.ctor()");
|
||||
initializeSoLoaderIfNecessary(applicationContext);
|
||||
|
||||
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext);
|
||||
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(applicationContext);
|
||||
|
||||
if (currentActivity != null) {
|
||||
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(currentActivity);
|
||||
}
|
||||
|
||||
// See {@code ReactInstanceManagerBuilder} for description of all flags here.
|
||||
mApplicationContext = applicationContext;
|
||||
@@ -924,6 +931,13 @@ public class ReactInstanceManager {
|
||||
|
||||
ReactContext currentReactContext = getCurrentReactContext();
|
||||
if (currentReactContext != null) {
|
||||
DisplayMetricsHolder.initScreenDisplayMetrics(currentReactContext);
|
||||
Activity currentActivity = currentReactContext.getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
DisplayMetricsHolder.initWindowDisplayMetrics(currentActivity);
|
||||
}
|
||||
|
||||
AppearanceModule appearanceModule =
|
||||
currentReactContext.getNativeModule(AppearanceModule.class);
|
||||
|
||||
|
||||
+2
@@ -5,6 +5,8 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ import java.util.List;
|
||||
*
|
||||
* @deprecated This class will be replaced by com.facebook.react.ReactHost in the New Architecture.
|
||||
*/
|
||||
@Deprecated
|
||||
@Deprecated(
|
||||
since = "This class is part of Legacy Architecture and will be removed in a future release")
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public abstract class ReactNativeHost {
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
@Deprecated("This class is deprecated and will be removed in the next major release.")
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
internal interface ReactPackageLogger {
|
||||
fun startProcessPackage(): Unit
|
||||
fun startProcessPackage()
|
||||
|
||||
fun endProcessPackage(): Unit
|
||||
fun endProcessPackage()
|
||||
}
|
||||
|
||||
+13
-7
@@ -136,9 +136,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
setRootViewTag(ReactRootViewTagGenerator.getNextRootViewTag());
|
||||
setClipChildren(false);
|
||||
|
||||
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
|
||||
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
|
||||
}
|
||||
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
|
||||
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,10 +204,15 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable ReactContext reactContext = getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventDispatcher eventDispatcher =
|
||||
UIManagerHelper.getEventDispatcher(getCurrentReactContext(), getUIManagerType());
|
||||
UIManagerHelper.getEventDispatcher(reactContext, getUIManagerType());
|
||||
if (eventDispatcher != null) {
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher);
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher, reactContext);
|
||||
if (childView != null && mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, eventDispatcher);
|
||||
}
|
||||
@@ -878,7 +882,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
private int mDeviceRotation = 0;
|
||||
|
||||
/* package */ CustomGlobalLayoutListener() {
|
||||
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext());
|
||||
DisplayMetricsHolder.initScreenDisplayMetricsIfNotInitialized(getContext());
|
||||
DisplayMetricsHolder.initWindowDisplayMetricsIfNotInitialized(getContext());
|
||||
mVisibleViewArea = new Rect();
|
||||
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
|
||||
}
|
||||
@@ -1001,7 +1006,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
|
||||
return;
|
||||
}
|
||||
mDeviceRotation = rotation;
|
||||
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
|
||||
DisplayMetricsHolder.initScreenDisplayMetrics(getContext());
|
||||
DisplayMetricsHolder.initWindowDisplayMetrics(getContext());
|
||||
emitOrientationChanged(rotation);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public abstract class AnimatedNode {
|
||||
@JvmField internal var BFSColor: Int = INITIAL_BFS_COLOR
|
||||
@JvmField internal var tag: Int = -1
|
||||
|
||||
internal fun addChild(child: AnimatedNode): Unit {
|
||||
internal fun addChild(child: AnimatedNode) {
|
||||
val currentChildren =
|
||||
children
|
||||
?: ArrayList<AnimatedNode>(DEFAULT_ANIMATED_NODE_CHILD_COUNT).also { children = it }
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ internal class DecayAnimation(config: ReadableMap) : AnimationDriver() {
|
||||
resetConfig(config)
|
||||
}
|
||||
|
||||
override fun resetConfig(config: ReadableMap): Unit {
|
||||
override fun resetConfig(config: ReadableMap) {
|
||||
velocity = config.getDouble("velocity")
|
||||
deceleration = config.getDouble("deceleration")
|
||||
startFrameTimeMillis = -1
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ internal class InterpolationAnimatedNode(config: ReadableMap) : ValueAnimatedNod
|
||||
}
|
||||
|
||||
private fun findRangeIndex(value: Double, ranges: DoubleArray): Int {
|
||||
var index: Int = 1
|
||||
var index = 1
|
||||
while (index < ranges.size - 1) {
|
||||
if (ranges[index] >= value) {
|
||||
break
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :
|
||||
*
|
||||
* @param viewTag The tag of the scroll view that has stopped scrolling
|
||||
*/
|
||||
public fun userDrivenScrollEnded(viewTag: Int): Unit {
|
||||
public fun userDrivenScrollEnded(viewTag: Int) {
|
||||
// ask to the Node Manager for all the native nodes listening to OnScroll event
|
||||
val nodeManager = nodesManagerRef.get() ?: return
|
||||
|
||||
|
||||
+21
-24
@@ -72,7 +72,7 @@ public class NativeAnimatedNodesManager(
|
||||
*
|
||||
* @param uiManagerType
|
||||
*/
|
||||
public fun initializeEventListenerForUIManagerType(@UIManagerType uiManagerType: Int): Unit {
|
||||
public fun initializeEventListenerForUIManagerType(@UIManagerType uiManagerType: Int) {
|
||||
val isEventListenerInitialized =
|
||||
when (uiManagerType) {
|
||||
UIManagerType.FABRIC -> eventListenerInitializedForFabric
|
||||
@@ -100,7 +100,7 @@ public class NativeAnimatedNodesManager(
|
||||
public fun hasActiveAnimations(): Boolean = activeAnimations.size() > 0 || updatedNodes.size() > 0
|
||||
|
||||
@UiThread
|
||||
public fun createAnimatedNode(tag: Int, config: ReadableMap): Unit {
|
||||
public fun createAnimatedNode(tag: Int, config: ReadableMap) {
|
||||
if (animatedNodes.get(tag) != null) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
"createAnimatedNode: Animated node [$tag] already exists")
|
||||
@@ -129,7 +129,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun updateAnimatedNodeConfig(tag: Int, config: ReadableMap?): Unit {
|
||||
public fun updateAnimatedNodeConfig(tag: Int, config: ReadableMap?) {
|
||||
val node =
|
||||
animatedNodes.get(tag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -143,16 +143,13 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun dropAnimatedNode(tag: Int): Unit {
|
||||
public fun dropAnimatedNode(tag: Int) {
|
||||
animatedNodes.remove(tag)
|
||||
updatedNodes.remove(tag)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun startListeningToAnimatedNodeValue(
|
||||
tag: Int,
|
||||
listener: AnimatedNodeValueListener?
|
||||
): Unit {
|
||||
public fun startListeningToAnimatedNodeValue(tag: Int, listener: AnimatedNodeValueListener?) {
|
||||
val node = animatedNodes[tag]
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -162,7 +159,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun stopListeningToAnimatedNodeValue(tag: Int): Unit {
|
||||
public fun stopListeningToAnimatedNodeValue(tag: Int) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -172,7 +169,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun setAnimatedNodeValue(tag: Int, value: Double): Unit {
|
||||
public fun setAnimatedNodeValue(tag: Int, value: Double) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -184,7 +181,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun setAnimatedNodeOffset(tag: Int, offset: Double): Unit {
|
||||
public fun setAnimatedNodeOffset(tag: Int, offset: Double) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -195,7 +192,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun flattenAnimatedNodeOffset(tag: Int): Unit {
|
||||
public fun flattenAnimatedNodeOffset(tag: Int) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -205,7 +202,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun extractAnimatedNodeOffset(tag: Int): Unit {
|
||||
public fun extractAnimatedNodeOffset(tag: Int) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -220,7 +217,7 @@ public class NativeAnimatedNodesManager(
|
||||
animatedNodeTag: Int,
|
||||
animationConfig: ReadableMap,
|
||||
endCallback: Callback?
|
||||
): Unit {
|
||||
) {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -298,7 +295,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun stopAnimation(animationId: Int): Unit {
|
||||
public fun stopAnimation(animationId: Int) {
|
||||
// in most of the cases there should never be more than a few active animations running at the
|
||||
// same time. Therefore it does not make much sense to create an animationId -> animation
|
||||
// object map that would require additional memory just to support the use-case of stopping
|
||||
@@ -342,7 +339,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun connectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int): Unit {
|
||||
public fun connectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int) {
|
||||
val parentNode =
|
||||
animatedNodes.get(parentNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -355,7 +352,7 @@ public class NativeAnimatedNodesManager(
|
||||
updatedNodes.put(childNodeTag, childNode)
|
||||
}
|
||||
|
||||
public fun disconnectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int): Unit {
|
||||
public fun disconnectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int) {
|
||||
val parentNode =
|
||||
animatedNodes.get(parentNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -369,7 +366,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun connectAnimatedNodeToView(animatedNodeTag: Int, viewTag: Int): Unit {
|
||||
public fun connectAnimatedNodeToView(animatedNodeTag: Int, viewTag: Int) {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -396,7 +393,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun disconnectAnimatedNodeFromView(animatedNodeTag: Int, viewTag: Int): Unit {
|
||||
public fun disconnectAnimatedNodeFromView(animatedNodeTag: Int, viewTag: Int) {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
@@ -409,7 +406,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun getValue(tag: Int, callback: Callback?): Unit {
|
||||
public fun getValue(tag: Int, callback: Callback?) {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
@@ -436,7 +433,7 @@ public class NativeAnimatedNodesManager(
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun restoreDefaultValues(animatedNodeTag: Int): Unit {
|
||||
public fun restoreDefaultValues(animatedNodeTag: Int) {
|
||||
val node = animatedNodes.get(animatedNodeTag) ?: return
|
||||
// Restoring default values needs to happen before UIManager operations so it is
|
||||
// possible the node hasn't been created yet if it is being connected and
|
||||
@@ -454,7 +451,7 @@ public class NativeAnimatedNodesManager(
|
||||
viewTag: Int,
|
||||
eventHandlerName: String,
|
||||
eventMapping: ReadableMap
|
||||
): Unit {
|
||||
) {
|
||||
val nodeTag = eventMapping.getInt("animatedValueTag")
|
||||
val node =
|
||||
animatedNodes.get(nodeTag)
|
||||
@@ -487,7 +484,7 @@ public class NativeAnimatedNodesManager(
|
||||
viewTag: Int,
|
||||
eventHandlerName: String,
|
||||
animatedValueTag: Int
|
||||
): Unit {
|
||||
) {
|
||||
val eventName = normalizeEventName(eventHandlerName)
|
||||
|
||||
eventDrivers
|
||||
@@ -550,7 +547,7 @@ public class NativeAnimatedNodesManager(
|
||||
* have already been visited.
|
||||
*/
|
||||
@UiThread
|
||||
public fun runUpdates(frameTimeNanos: Long): Unit {
|
||||
public fun runUpdates(frameTimeNanos: Long) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
var hasFinishedAnimations = false
|
||||
|
||||
|
||||
+4
-4
@@ -27,21 +27,21 @@ internal open class ValueAnimatedNode(config: ReadableMap? = null) : AnimatedNod
|
||||
|
||||
open fun getAnimatedObject(): Any? = null
|
||||
|
||||
fun flattenOffset(): Unit {
|
||||
fun flattenOffset() {
|
||||
nodeValue += offset
|
||||
offset = 0.0
|
||||
}
|
||||
|
||||
fun extractOffset(): Unit {
|
||||
fun extractOffset() {
|
||||
offset += nodeValue
|
||||
nodeValue = 0.0
|
||||
}
|
||||
|
||||
fun onValueUpdate(): Unit {
|
||||
fun onValueUpdate() {
|
||||
valueListener?.onValueUpdate(getValue() - offset, offset)
|
||||
}
|
||||
|
||||
fun setValueListener(listener: AnimatedNodeValueListener?): Unit {
|
||||
fun setValueListener(listener: AnimatedNodeValueListener?) {
|
||||
valueListener = listener
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -78,13 +78,17 @@ public interface CatalystInstance : MemoryPressureListener, JSInstance, JSBundle
|
||||
* defined as there being some non-zero number of calls to JS that haven't resolved via a
|
||||
* onBatchCompleted call. The listener should be purely passive and not affect application logic.
|
||||
*/
|
||||
public fun addBridgeIdleDebugListener(listener: NotThreadSafeBridgeIdleDebugListener)
|
||||
public fun addBridgeIdleDebugListener(
|
||||
@Suppress("DEPRECATION") listener: NotThreadSafeBridgeIdleDebugListener
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes a NotThreadSafeBridgeIdleDebugListener previously added with
|
||||
* [addBridgeIdleDebugListener]
|
||||
*/
|
||||
public fun removeBridgeIdleDebugListener(listener: NotThreadSafeBridgeIdleDebugListener)
|
||||
public fun removeBridgeIdleDebugListener(
|
||||
@Suppress("DEPRECATION") listener: NotThreadSafeBridgeIdleDebugListener
|
||||
)
|
||||
|
||||
/** This method registers the file path of an additional JS segment by its ID. */
|
||||
public fun registerSegment(segmentId: Int, path: String)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user