From 15774845a5e31db824b94288751c05937be52a9c Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Wed, 23 Jul 2025 08:17:36 -0700 Subject: [PATCH 1/2] skip bytecode support for hermes Summary: Changelog: [Internal] Currently tests that depend on hermes dev bytecode will fail because in OSS this case was not handled. We need to skip them for now before we integrate hermes compiler properly. Differential Revision: D78750791 --- private/react-native-fantom/runner/runner.js | 76 +++++++++++++------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/private/react-native-fantom/runner/runner.js b/private/react-native-fantom/runner/runner.js index 6d1b3fde81a7b1..ec1f7a46da4148 100644 --- a/private/react-native-fantom/runner/runner.js +++ b/private/react-native-fantom/runner/runner.js @@ -8,7 +8,11 @@ * @format */ -import type {FailureDetail, TestSuiteResult} from '../runtime/setup'; +import type { + FailureDetail, + TestCaseResult, + TestSuiteResult, +} from '../runtime/setup'; import type {TestSnapshotResults} from '../runtime/snapshotContext'; import type { AsyncCommandResult, @@ -218,25 +222,38 @@ module.exports = async function runTest( const testResultsByConfig = []; + const skippedTestResults = ({ + ancestorTitles, + title, + }: { + ancestorTitles: string[], + title: string, + }) => [ + { + ancestorTitles, + duration: 0, + failureDetails: [] as Array, + failureMessages: [] as Array, + fullName: title, + numPassingAsserts: 0, + snapshotResults: {} as TestSnapshotResults, + status: 'pending' as TestCaseResult['status'], + testFilePath: testPath, + title, + }, + ]; + for (const testConfig of testConfigs) { if ( EnvironmentOptions.isOSS && testConfig.mode === FantomTestConfigMode.Optimized ) { - testResultsByConfig.push([ - { + testResultsByConfig.push( + skippedTestResults({ ancestorTitles: ['"@fantom_mode opt" in docblock'], - duration: 0, - failureDetails: [] as Array, - failureMessages: [] as Array, - fullName: 'Optimized mode is not yet supoprted in OSS', - numPassingAsserts: 0, - snapshotResults: {} as TestSnapshotResults, - status: 'pending' as 'passed' | 'failed' | 'pending', - testFilePath: testPath, - title: 'Optimized mode is not yet supoprted in OSS', - }, - ]); + title: 'Optimized mode is not yet supported in OSS', + }), + ); continue; } @@ -244,22 +261,27 @@ module.exports = async function runTest( EnvironmentOptions.isOSS && testConfig.hermesVariant !== HermesVariantEnum.Hermes ) { - testResultsByConfig.push([ - { + testResultsByConfig.push( + skippedTestResults({ ancestorTitles: [ '"@fantom_hermes_variant static_hermes" in docblock (shermes 🧪)', ], - duration: 0, - failureDetails: [] as Array, - failureMessages: [] as Array, - fullName: 'Static Hermes is not yet supoprted in OSS', - numPassingAsserts: 0, - snapshotResults: {} as TestSnapshotResults, - status: 'pending' as 'passed' | 'failed' | 'pending', - testFilePath: testPath, - title: 'Static Hermes is not yet supoprted in OSS', - }, - ]); + title: 'Static Hermes is not yet supported in OSS', + }), + ); + continue; + } + + if ( + EnvironmentOptions.isOSS && + testConfig.mode !== FantomTestConfigMode.DevelopmentWithSource + ) { + testResultsByConfig.push( + skippedTestResults({ + ancestorTitles: ['"@fantom_mode dev-bytecode" in docblock'], + title: 'Hermes bytecode is not yet supported in OSS', + }), + ); continue; } From 5d8df57cc7032bd84a8c95e42787bd3c59feea68 Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Wed, 23 Jul 2025 09:31:41 -0700 Subject: [PATCH 2/2] Move some tests into fb verse Summary: Changelog: [Internal] Failing oss tests due to different React Differential Revision: D78755895 --- .../LogBox/__tests__/LogBox-itest.js | 826 ------------------ .../__tests__/InterruptibleRendering-itest.js | 236 ----- .../__tests__/ReactFabric-Suspense-itest.js | 235 ----- .../webapis/__tests__/FragmentRefs-itest.js | 82 -- 4 files changed, 1379 deletions(-) delete mode 100644 packages/react-native/Libraries/LogBox/__tests__/LogBox-itest.js delete mode 100644 packages/react-native/Libraries/ReactNative/__tests__/InterruptibleRendering-itest.js delete mode 100644 packages/react-native/Libraries/ReactNative/__tests__/ReactFabric-Suspense-itest.js delete mode 100644 packages/react-native/src/private/webapis/__tests__/FragmentRefs-itest.js diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-itest.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-itest.js deleted file mode 100644 index 13dcb0aa4d8912..00000000000000 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-itest.js +++ /dev/null @@ -1,826 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -import 'react-native/Libraries/Core/InitializeCore'; - -import {renderLogBox} from './fantomHelpers'; -import * as Fantom from '@react-native/fantom'; -import * as React from 'react'; -import {useEffect} from 'react'; -import {LogBox, Text, View} from 'react-native'; - -// If a test uses this, it should have a component frame. -// This is a bug we'll fix in a followup. -const BUG_WITH_COMPONENT_FRAMES: [] = []; - -// Disable the logic to make sure that LogBox is not installed in tests. -Fantom.setLogBoxCheckEnabled(false); - -describe('LogBox', () => { - let originalConsoleError; - let originalConsoleWarn; - let mockError; - let mockWarn; - - beforeAll(() => { - originalConsoleError = console.error; - originalConsoleWarn = console.warn; - }); - - beforeEach(() => { - mockError = jest.fn((...args) => { - originalConsoleError(...args); - }); - mockWarn = jest.fn((...args) => { - originalConsoleWarn(...args); - }); - // $FlowExpectedError[cannot-write] - console.error = mockError; - // $FlowExpectedError[prop-missing] - console.error.displayName = 'MockConsoleErrorForTesting'; - // $FlowExpectedError[cannot-write] - console.warn = mockWarn; - // $FlowExpectedError[prop-missing] - console.warn.displayName = 'MockConsoleWarnForTesting'; - - LogBox.install(); - LogBox.clearAllLogs(); - LogBox.ignoreAllLogs(false); - }); - - afterEach(() => { - // $FlowExpectedError[cannot-write] - console.error = originalConsoleError; - // $FlowExpectedError[cannot-write] - console.warn = originalConsoleWarn; - }); - - type ErrorState = {hasError: boolean}; - type ErrorProps = {children: React.Node}; - class ErrorBoundary extends React.Component { - state: ErrorState = {hasError: false}; - - static getDerivedStateFromError(error: Error): ErrorState { - // Update state so the next render will show the fallback UI. - return {hasError: true}; - } - - render(): React.Node { - if (this.state.hasError) { - return Error; - } - return this.props.children; - } - } - - describe('Error display and interactions', () => { - it('does not render LogBox if there are no errors', () => { - const logBox = renderLogBox(); - - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('shows a soft error, and can dismiss the notification', () => { - function TestComponent() { - console.error('HIT'); - } - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - - // Dismiss - logBox.dimissNotification(); - - // All logs should be cleared. - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('dedupes soft errors', () => { - function TestComponent() { - // Important! There should be two idential logs. - console.error('HIT'); - console.error('HIT'); - } - const logBox = renderLogBox(); - - // There should only be one notification. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - }); - - it('show a notification, opens it, and dismisses', () => { - function TestComponent() { - console.error('HIT'); - } - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - - // Open LogBox. - logBox.openNotification(); - - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Console Error', - message: 'HIT', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['TestComponent'], - isDismissable: true, - }); - - // Dismiss LogBox. - logBox.dismissInspector(); - - // All logs should be cleared. - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('shows a notification, opens it, and minimizes', () => { - function TestComponent() { - console.error('HIT'); - } - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - - // Open LogBox. - logBox.openNotification(); - - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Console Error', - message: 'HIT', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['TestComponent'], - isDismissable: true, - }); - - // Minimize LogBox. - logBox.mimimizeInspector(); - - // Inspector should be closed, but notification is still there. - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - }); - - it('shows multiple errors, opens, navigates, minimizes, and dismisses', () => { - function TestComponent() { - console.error('HIT in render'); - useEffect(() => { - console.error('HIT in effect'); - }); - } - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '2', - message: 'HIT in effect', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the most recent error. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 2 of 2', - title: 'Console Error', - message: 'HIT in effect', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['anonymous'], - isDismissable: true, - }); - - // Navigate to the next error, which is 1 of 2. - logBox.nextLog(); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 2', - title: 'Console Error', - message: 'HIT in render', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['TestComponent'], - isDismissable: true, - }); - - // Navigate to the previous error, which is 2 of 2. - logBox.nextLog(); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 2 of 2', - title: 'Console Error', - message: 'HIT in effect', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['anonymous'], - isDismissable: true, - }); - - // Minimize, there should still be 2 logs. - logBox.mimimizeInspector(); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '2', - message: 'HIT in effect', - }); - - // Open, and dismiss one. There should still be one. - logBox.openNotification(); - logBox.dismissInspector(); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Console Error', - message: 'HIT in render', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['TestComponent'], - isDismissable: true, - }); - - // Dismiss, and there should be no logs. - logBox.dimissNotification(); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - }); - - describe('LogBox.uninstall and LogBox.isInstalled()', () => { - it('does not render console errors after uninstall', () => { - function TestComponent() { - console.error('HIT'); - } - expect(LogBox.isInstalled()).toBe(true); - - // Uninstall and render - LogBox.uninstall(); - expect(LogBox.isInstalled()).toBe(false); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - - // Install and render again - LogBox.install(); - expect(LogBox.isInstalled()).toBe(true); - logBox = renderLogBox(); - - // Should be a log. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - }); - - it('does not render thrown errors after uninstall', () => { - function TestComponent() { - throw new Error('HIT'); - } - expect(LogBox.isInstalled()).toBe(true); - - // Uninstall and render - LogBox.uninstall(); - expect(LogBox.isInstalled()).toBe(false); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - - // Install and render again - LogBox.install(); - expect(LogBox.isInstalled()).toBe(true); - logBox = renderLogBox(); - - // Should pop a dialog. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'HIT', - stackFrames: ['TestComponent'], - componentStackFrames: ['', '', ''], - isDismissable: true, - }); - - logBox.nextLog(); - }); - }); - - describe('LogBox.ignoreAllLogs', () => { - it('ignores console errors after ignoreAllLogs', () => { - function TestComponent() { - console.error('HIT'); - } - - // Uninstall and render - LogBox.ignoreAllLogs(); - let logBox = renderLogBox(); - - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - - // Reset and render again - LogBox.ignoreAllLogs(false); - expect(LogBox.isInstalled()).toBe(true); - logBox = renderLogBox(); - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT', - }); - }); - - it('does not ignore thrown errors after ignoreAllLogs', () => { - function TestComponent() { - throw new Error('HIT'); - } - - LogBox.ignoreAllLogs(); - let logBox = renderLogBox(); - - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'HIT', - stackFrames: ['TestComponent'], - componentStackFrames: ['', '', ''], - isDismissable: true, - }); - - logBox.nextLog(); - }); - }); - - describe('LogBox.ignoreLogs', () => { - it('ignores console errors after ignoreLogs (string)', () => { - function TestComponent() { - console.error('HIT - should be ignored (string)'); - console.error('HIT - should be ignored (regex)'); - } - - // Ignore logs and render - LogBox.ignoreLogs([ - 'HIT - should be ignored (string)', - /HIT - should be ignored/, - ]); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('ignores console errors after ignoreLogs (regex)', () => { - function TestComponent() { - console.error('HIT - should be ignored (regex)'); - } - - // Ignore logs and render - LogBox.ignoreLogs([/HIT - should be ignored/]); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('ignores thrown errors after ignoreLogs (string)', () => { - function TestComponent() { - throw new Error('THROW - should be ignored (string)'); - } - - // Ignore logs and render - LogBox.ignoreLogs(['THROW - should be ignored (string)']); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - - it('ignores thrown errors after ignoreLogs (regex)', () => { - function TestComponent() { - throw new Error('THROW - should be ignored (regex)'); - } - - // Ignore logs and render. - LogBox.ignoreLogs([/THROW - should be ignored/]); - let logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - }); - - describe('LogBox.clearAllLogs', () => { - it('clears soft errors and thrown errors after clearAllLogs', () => { - function TestComponent() { - console.error('HIT'); - throw new Error('THROW'); - } - - // Render with soft error and thrown error. - const logBox = renderLogBox(); - - // Should show both. - // Note: this should only have 2 logs, but a bug renders an extra log. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getNotificationUI()).toBe(null); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 2 of 2', - title: 'Render Error', - message: 'THROW', - stackFrames: ['TestComponent'], - componentStackFrames: ['', '', ''], - isDismissable: true, - }); - - // Clear all logs. - Fantom.runTask(() => { - LogBox.clearAllLogs(); - }); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getInspectorUI()).toBe(null); - expect(logBox.getNotificationUI()).toBe(null); - }); - }); - - describe('LogBox.addLog and LogBox.addException', () => { - it('adds a log an exception', () => { - const logBox = renderLogBox(); - - // Should be no logs. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toBe(null); - expect(logBox.getInspectorUI()).toBe(null); - - // Add a log and an exception. - Fantom.runTask(() => { - LogBox.addLog({ - level: 'error', - category: 'HIT', - message: { - content: 'HIT', - substitutions: [], - }, - stack: 'at TestComponent', - componentStack: [ - { - content: 'TestComponent', - location: { - row: 1, - column: 1, - }, - fileName: 'file.js', - collapse: false, - }, - ], - componentStackType: 'stack', - }); - LogBox.addException({ - message: 'THROW', - originalMessage: 'THROW', - isComponentError: false, - name: 'Throw', - componentStack: ' at TestComponent', - stack: [ - { - column: 1, - file: 'file.js', - lineNumber: 1, - methodName: 'TestComponent', - collapse: false, - }, - ], - id: 1, - isFatal: false, - }); - }); - - // Should show both. - expect(logBox.getNotificationUI()).toEqual({ - count: '2', - message: 'THROW', - }); - }); - }); - - describe('Display for different types of errors', () => { - it('shows a console error in render', () => { - function TestComponent() { - console.error('HIT in render'); - } - - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT in render', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the error. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Console Error', - message: 'HIT in render', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['TestComponent'], - isDismissable: true, - }); - }); - - it('shows a soft error in an effect', () => { - function TestComponent() { - useEffect(() => { - console.error('HIT in effect'); - }); - } - const logBox = renderLogBox(); - - // Console error should not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: 'HIT in effect', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the error. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Console Error', - message: 'HIT in effect', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - stackFrames: ['anonymous'], - isDismissable: true, - }); - }); - - it('shows an uncaught error in render', () => { - function TestComponent() { - throw new Error('THROWN in render'); - } - const logBox = renderLogBox(); - - // Uncaught errors pop a dialog. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'THROWN in render', - componentStackFrames: ['', '', ''], - stackFrames: ['TestComponent'], - isDismissable: true, - }); - }); - - it('shows an uncaught error in an effect', () => { - function TestComponent() { - useEffect(() => { - throw new Error('THROWN in effect'); - }); - } - const logBox = renderLogBox(); - - // Uncaught errors pop a dialog. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'THROWN in effect', - componentStackFrames: ['', '', ''], - stackFrames: ['anonymous'], - isDismissable: true, - }); - }); - - it('shows a caught error in render', () => { - function TestComponent() { - throw new Error('THROWN in render'); - } - const logBox = renderLogBox( - - - , - ); - - // Caught errors pop a dialog. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'THROWN in render', - componentStackFrames: [ - '', - '', - '', - ], - stackFrames: ['TestComponent'], - isDismissable: true, - }); - }); - - it('shows a caught error in an effect', () => { - function TestComponent() { - useEffect(() => { - throw new Error('THROWN in effect'); - }); - } - const logBox = renderLogBox( - - - , - ); - - // Caught errors pop a dialog. - expect(logBox.isOpen()).toBe(true); - expect(logBox.getInspectorUI()).toEqual({ - header: 'Log 1 of 1', - title: 'Render Error', - message: 'THROWN in effect', - componentStackFrames: [ - '', - '', - '', - ], - stackFrames: ['anonymous'], - isDismissable: true, - }); - }); - - it('shows a recoverable error in render', () => { - let rendered = false; - function TestComponent() { - if (!rendered) { - rendered = true; - throw new Error('THROWN in render'); - } - } - const logBox = renderLogBox( - - - , - ); - - // Recoverable errors do not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: - 'There was an error during concurrent rendering ' + - 'but React was able to recover by instead synchronously ' + - 'rendering the entire root.', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the error. - expect(logBox.isOpen()).toBe(true); - const ui = logBox.getInspectorUI(); - delete ui?.stackFrames; // too big to show - expect(ui).toEqual({ - header: 'Log 1 of 1', - // This seems like a bug, should be "Render Error". - title: 'Console Error', - message: - 'There was an error during concurrent rendering ' + - 'but React was able to recover by instead synchronously ' + - 'rendering the entire root.', - componentStackFrames: BUG_WITH_COMPONENT_FRAMES, - isDismissable: true, - }); - }); - - it('shows a key error (with interpolation)', () => { - function TestComponent() { - return [1, 2].map(i => {i}); - } - const logBox = renderLogBox(); - - // Recoverable errors do not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: - 'Each child in a list should have a unique "key" prop.' + - '\n\nCheck the top-level render call using . ' + - 'It was passed a child from TestComponent. ' + - 'See https://react.dev/link/warning-keys for more information.', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the error. - expect(logBox.isOpen()).toBe(true); - const ui = logBox.getInspectorUI(); - delete ui?.stackFrames; // too big to show - expect(ui).toEqual({ - header: 'Log 1 of 1', - // This seems like a bug, should be "Render Error". - title: 'Console Error', - message: - 'Each child in a list should have a unique "key" prop.' + - '\n\nCheck the top-level render call using . ' + - 'It was passed a child from TestComponent. ' + - 'See https://react.dev/link/warning-keys for more information.', - componentStackFrames: ['', ''], - isDismissable: true, - }); - }); - - it('shows a fragment error (with interpolation)', () => { - function TestComponent() { - return ( - // $FlowExpectedError[prop-missing] - - Bar - - ); - } - const logBox = renderLogBox(); - - // Recoverable errors do not pop a dialog. - expect(logBox.isOpen()).toBe(false); - expect(logBox.getNotificationUI()).toEqual({ - count: '!', - message: - 'Invalid prop `invalid` supplied to `React.Fragment`. React.Fragment can only have `key`, `ref`, and `children` props.', - }); - - // Open LogBox. - logBox.openNotification(); - - // Should show the error. - expect(logBox.isOpen()).toBe(true); - const ui = logBox.getInspectorUI(); - delete ui?.stackFrames; // too big to show - expect(ui).toEqual({ - header: 'Log 1 of 1', - // This seems like a bug, should be "Render Error". - title: 'Console Error', - message: - 'Invalid prop `invalid` supplied to `React.Fragment`. React.Fragment can only have `key`, `ref`, and `children` props.', - componentStackFrames: [''], - isDismissable: true, - }); - }); - }); -}); diff --git a/packages/react-native/Libraries/ReactNative/__tests__/InterruptibleRendering-itest.js b/packages/react-native/Libraries/ReactNative/__tests__/InterruptibleRendering-itest.js deleted file mode 100644 index e1ed6b4e29fe0b..00000000000000 --- a/packages/react-native/Libraries/ReactNative/__tests__/InterruptibleRendering-itest.js +++ /dev/null @@ -1,236 +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. - * - * @fantom_flags fixMappingOfEventPrioritiesBetweenFabricAndReact:true - * @flow strict-local - * @format - */ - -import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; - -import type {HostInstance} from 'react-native'; - -import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance'; -import * as Fantom from '@react-native/fantom'; -import * as React from 'react'; -import { - createRef, - startTransition, - useDeferredValue, - useEffect, - useState, -} from 'react'; -import {Text, TextInput} from 'react-native'; -import {NativeEventCategory} from 'react-native/src/private/testing/fantom/specs/NativeFantom'; -import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; - -function ensureReactNativeElement(value: mixed): ReactNativeElement { - return ensureInstance(value, ReactNativeElement); -} - -describe('discrete event category', () => { - it('interrupts React rendering and higher priority update is committed first', () => { - const root = Fantom.createRoot(); - const textInputRef = createRef(); - const importantTextNodeRef = createRef(); - const deferredTextNodeRef = createRef(); - let interruptRendering = false; - let effectMock = jest.fn(); - let afterUpdate; - - function App(props: {text: string}) { - const [text, setText] = useState('initial text'); - - let deferredText = useDeferredValue(props.text); - - if (interruptRendering) { - interruptRendering = false; - const element = ensureReactNativeElement(textInputRef.current); - Fantom.dispatchNativeEvent( - element, - 'change', - { - text: 'update from native', - }, - { - category: NativeEventCategory.Discrete, - }, - ); - // We must schedule a task that is run right after the above native event is - // processed to be able to observe the results of rendering. - Fantom.scheduleTask(afterUpdate); - } - - useEffect(() => { - effectMock({text, deferredText}); - }, [text, deferredText]); - - return ( - <> - - Important text: {text} - Deferred text: {deferredText} - - ); - } - - Fantom.runTask(() => { - root.render(); - }); - - const importantTextNativeElement = ensureReactNativeElement( - importantTextNodeRef.current, - ); - const deferredTextNativeElement = ensureReactNativeElement( - deferredTextNodeRef.current, - ); - - expect(importantTextNativeElement.textContent).toBe( - 'Important text: initial text', - ); - expect(deferredTextNativeElement.textContent).toBe( - 'Deferred text: first render', - ); - - interruptRendering = true; - - let isImportantTextUpdatedBeforeDeferred = false; - - afterUpdate = () => { - isImportantTextUpdatedBeforeDeferred = - importantTextNativeElement.textContent === - 'Important text: update from native' && - deferredTextNativeElement.textContent === 'Deferred text: first render'; - }; - - Fantom.runTask(() => { - startTransition(() => { - root.render(); - }); - }); - - expect(isImportantTextUpdatedBeforeDeferred).toBe(true); - - expect(effectMock).toHaveBeenCalledTimes(3); - expect(effectMock.mock.calls[0][0]).toEqual({ - text: 'initial text', - deferredText: 'first render', - }); - expect(effectMock.mock.calls[1][0]).toEqual({ - text: 'update from native', - deferredText: 'first render', - }); - expect(effectMock.mock.calls[2][0]).toEqual({ - text: 'update from native', - deferredText: 'transition', - }); - expect(importantTextNativeElement.textContent).toBe( - 'Important text: update from native', - ); - expect(deferredTextNativeElement.textContent).toBe( - 'Deferred text: transition', - ); - }); -}); - -describe('continuous event category', () => { - it('interrupts React rendering but update from continous event is delayed', () => { - const root = Fantom.createRoot(); - const textInputRef = createRef(); - const importantTextNodeRef = createRef(); - const deferredTextNodeRef = createRef(); - let interruptRendering = false; - let effectMock = jest.fn(); - - function App(props: {text: string}) { - const [text, setText] = useState('initial text'); - - let deferredText = useDeferredValue(props.text); - - if (interruptRendering) { - interruptRendering = false; - const element = ensureReactNativeElement(textInputRef.current); - Fantom.dispatchNativeEvent( - element, - 'selectionChange', - { - selection: { - start: 1, - end: 5, - }, - }, - { - category: NativeEventCategory.Continuous, - }, - ); - } - useEffect(() => { - effectMock({text, deferredText}); - }, [text, deferredText]); - - return ( - <> - { - setText( - `start: ${event.nativeEvent.selection.start}, end: ${event.nativeEvent.selection.end}`, - ); - }} - ref={textInputRef} - /> - Important text: {text} - Deferred text: {deferredText} - - ); - } - - Fantom.runTask(() => { - root.render(); - }); - - const importantTextNativeElement = ensureReactNativeElement( - importantTextNodeRef.current, - ); - const deferredTextNativeElement = ensureReactNativeElement( - deferredTextNodeRef.current, - ); - - expect(importantTextNativeElement.textContent).toBe( - 'Important text: initial text', - ); - expect(deferredTextNativeElement.textContent).toBe( - 'Deferred text: first render', - ); - - interruptRendering = true; - - Fantom.runTask(() => { - startTransition(() => { - root.render(); - }); - }); - - expect(effectMock).toHaveBeenCalledTimes(3); - expect(effectMock.mock.calls[0][0]).toEqual({ - text: 'initial text', - deferredText: 'first render', - }); - expect(effectMock.mock.calls[1][0]).toEqual({ - text: 'start: 1, end: 5', - deferredText: 'first render', - }); - expect(effectMock.mock.calls[2][0]).toEqual({ - text: 'start: 1, end: 5', - deferredText: 'transition', - }); - expect(importantTextNativeElement.textContent).toBe( - 'Important text: start: 1, end: 5', - ); - expect(deferredTextNativeElement.textContent).toBe( - 'Deferred text: transition', - ); - }); -}); diff --git a/packages/react-native/Libraries/ReactNative/__tests__/ReactFabric-Suspense-itest.js b/packages/react-native/Libraries/ReactNative/__tests__/ReactFabric-Suspense-itest.js deleted file mode 100644 index 1fa876d4578be4..00000000000000 --- a/packages/react-native/Libraries/ReactNative/__tests__/ReactFabric-Suspense-itest.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; - -import * as Fantom from '@react-native/fantom'; -import * as React from 'react'; -import {Suspense, startTransition} from 'react'; -import {View} from 'react-native'; - -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 { - await new Promise(resolve => { - resolveFunction = resolve; - }); - return { - color: 'green', - }; -} - -async function getRedSquareData(): Promise { - await new Promise(resolve => { - resolveFunction = resolve; - }); - return { - color: 'red', - }; -} - -const cache = new Map(); - -async function getData(squareId: SquareId): Promise { - switch (squareId) { - case SquareId.Green: - return await getGreenSquareData(); - case SquareId.Red: - return await getRedSquareData(); - } -} - -async function fetchData(squareId: SquareId): Promise { - 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 ; -} - -function GreenSquare() { - return ; -} - -function RedSquare() { - return ; -} - -function Fallback() { - return ; -} - -describe('Suspense', () => { - it('shows fallback if data is not available', () => { - cache.clear(); - const root = Fantom.createRoot(); - - Fantom.runTask(() => { - root.render( - }> - - , - ); - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "View", nativeID: "suspense-fallback"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "suspense-fallback"}', - ]); - - expect(resolveFunction).not.toBeNull(); - Fantom.runTask(() => { - resolveFunction?.(); - resolveFunction = null; - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: "suspense-fallback"}', - 'Delete {type: "View", nativeID: "suspense-fallback"}', - 'Create {type: "View", nativeID: "square-with-data-green"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-green"}', - ]); - - Fantom.runTask(() => { - root.render( - }> - - , - ); - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-green"}', - 'Delete {type: "View", nativeID: "square-with-data-green"}', - 'Create {type: "View", nativeID: "suspense-fallback"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "suspense-fallback"}', - ]); - - expect(resolveFunction).not.toBeNull(); - Fantom.runTask(() => { - resolveFunction?.(); - resolveFunction = null; - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: "suspense-fallback"}', - 'Delete {type: "View", nativeID: "suspense-fallback"}', - 'Create {type: "View", nativeID: "square-with-data-red"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-red"}', - ]); - - Fantom.runTask(() => { - root.render( - }> - - , - ); - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-red"}', - 'Delete {type: "View", nativeID: "square-with-data-red"}', - 'Create {type: "View", nativeID: "square-with-data-green"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-green"}', - ]); - - expect(resolveFunction).toBeNull(); - }); - - 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 ( - }> - {props.color === 'green' ? : } - - ); - } - - Fantom.runTask(() => { - root.render(); - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "RootView", nativeID: (root)}', - 'Create {type: "View", nativeID: "square-with-data-green"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-green"}', - ]); - - expect(resolveFunction).toBeNull(); - Fantom.runTask(() => { - startTransition(() => { - root.render(); - }); - }); - - // Green square is still mounted. Fallback is not shown to the user. - expect(root.takeMountingManagerLogs()).toEqual([]); - - expect(resolveFunction).not.toBeNull(); - Fantom.runTask(() => { - resolveFunction?.(); - resolveFunction = null; - }); - - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-green"}', - 'Delete {type: "View", nativeID: "square-with-data-green"}', - 'Create {type: "View", nativeID: "square-with-data-red"}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "square-with-data-red"}', - ]); - }); -}); diff --git a/packages/react-native/src/private/webapis/__tests__/FragmentRefs-itest.js b/packages/react-native/src/private/webapis/__tests__/FragmentRefs-itest.js deleted file mode 100644 index 1d8c13f236a9c2..00000000000000 --- a/packages/react-native/src/private/webapis/__tests__/FragmentRefs-itest.js +++ /dev/null @@ -1,82 +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/fantom/src/setUpDefaultReactNativeEnvironment'; - -import * as Fantom from '@react-native/fantom'; -import * as React from 'react'; -import {View} from 'react-native'; -import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver'; - -setUpIntersectionObserver(); - -describe('Fragment Refs', () => { - describe('observers', () => { - it('attaches intersection observers to children', () => { - let logs: Array = []; - const root = Fantom.createRoot({ - viewportHeight: 1000, - viewportWidth: 1000, - }); - // $FlowFixMe[cannot-resolve-name] oss doesn't have this - const observer = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - logs.push(`show:${entry.target.id}`); - } else { - logs.push(`hide:${entry.target.id}`); - } - }); - }); - function Test({showB}: {showB: boolean}) { - // $FlowFixMe[cannot-resolve-name] oss doesn't have this - const fragmentRef = React.useRef(null); - React.useEffect(() => { - fragmentRef.current?.observeUsing(observer); - const lastRefValue = fragmentRef.current; - return () => { - lastRefValue?.unobserveUsing(observer); - }; - }, []); - return ( - - {/* $FlowFixMe oss doesn't have this */} - - - {showB && ( - - )} - - - ); - } - - Fantom.runTask(() => { - root.render(); - }); - expect(logs).toEqual(['show:childA']); - - // Reveal child and expect it to be observed and intersecting - logs = []; - Fantom.runTask(() => { - root.render(); - }); - expect(logs).toEqual(['show:childB']); - - // Hide child and expect it to still be observed, no longer intersecting - logs = []; - Fantom.runTask(() => { - root.render(); - }); - expect(logs).toEqual(['hide:childB']); - }); - }); -});