Compare commits

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

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