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 13dcb0aa4d8..00000000000 --- 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 e1ed6b4e29f..00000000000 --- 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 1fa876d4578..00000000000 --- 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 1d8c13f236a..00000000000 --- 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']); - }); - }); -});