Move some tests into fb verse (#52794)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52794

Changelog: [Internal]
Failing oss tests due to different React

Reviewed By: rubennorte

Differential Revision: D78755895

fbshipit-source-id: 6a1c2f5baf8ecc0c9116dc739a8f767ba60fff8a
This commit is contained in:
Andrew Datsenko
2025-07-24 07:23:28 -07:00
committed by Facebook GitHub Bot
parent 273c2d842d
commit b2d9f5f280
4 changed files with 0 additions and 1379 deletions
@@ -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<ErrorProps, ErrorState> {
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 <Text>Error</Text>;
}
return this.props.children;
}
}
describe('Error display and interactions', () => {
it('does not render LogBox if there are no errors', () => {
const logBox = renderLogBox(<View />);
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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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: ['<TestComponent />', '<View />', '<View />'],
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(<TestComponent />);
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(<TestComponent />);
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(<TestComponent />);
expect(logBox.isOpen()).toBe(true);
expect(logBox.getInspectorUI()).toEqual({
header: 'Log 1 of 1',
title: 'Render Error',
message: 'HIT',
stackFrames: ['TestComponent'],
componentStackFrames: ['<TestComponent />', '<View />', '<View />'],
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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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: ['<TestComponent />', '<View />', '<View />'],
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(<View />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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(<TestComponent />);
// 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: ['<TestComponent />', '<View />', '<View />'],
stackFrames: ['TestComponent'],
isDismissable: true,
});
});
it('shows an uncaught error in an effect', () => {
function TestComponent() {
useEffect(() => {
throw new Error('THROWN in effect');
});
}
const logBox = renderLogBox(<TestComponent />);
// 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: ['<TestComponent />', '<View />', '<View />'],
stackFrames: ['anonymous'],
isDismissable: true,
});
});
it('shows a caught error in render', () => {
function TestComponent() {
throw new Error('THROWN in render');
}
const logBox = renderLogBox(
<ErrorBoundary>
<TestComponent />
</ErrorBoundary>,
);
// 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: [
'<TestComponent />',
'<ErrorBoundary />',
'<View />',
],
stackFrames: ['TestComponent'],
isDismissable: true,
});
});
it('shows a caught error in an effect', () => {
function TestComponent() {
useEffect(() => {
throw new Error('THROWN in effect');
});
}
const logBox = renderLogBox(
<ErrorBoundary>
<TestComponent />
</ErrorBoundary>,
);
// 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: [
'<TestComponent />',
'<ErrorBoundary />',
'<View />',
],
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(
<ErrorBoundary>
<TestComponent />
</ErrorBoundary>,
);
// 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 => <Text>{i}</Text>);
}
const logBox = renderLogBox(<TestComponent />);
// 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 <TestComponent>. ' +
'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 <TestComponent>. ' +
'It was passed a child from TestComponent. ' +
'See https://react.dev/link/warning-keys for more information.',
componentStackFrames: ['<anonymous />', '<TestComponent />'],
isDismissable: true,
});
});
it('shows a fragment error (with interpolation)', () => {
function TestComponent() {
return (
// $FlowExpectedError[prop-missing]
<React.Fragment invalid="foo">
<Text>Bar</Text>
</React.Fragment>
);
}
const logBox = renderLogBox(<TestComponent />);
// 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: ['<TestComponent />'],
isDismissable: true,
});
});
});
});
@@ -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<HostInstance>();
const importantTextNodeRef = createRef<HostInstance>();
const deferredTextNodeRef = createRef<HostInstance>();
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 (
<>
<TextInput onChangeText={setText} ref={textInputRef} />
<Text ref={importantTextNodeRef}>Important text: {text}</Text>
<Text ref={deferredTextNodeRef}>Deferred text: {deferredText}</Text>
</>
);
}
Fantom.runTask(() => {
root.render(<App text={'first 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(<App text={'transition'} />);
});
});
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<HostInstance>();
const importantTextNodeRef = createRef<HostInstance>();
const deferredTextNodeRef = createRef<HostInstance>();
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 (
<>
<TextInput
onSelectionChange={event => {
setText(
`start: ${event.nativeEvent.selection.start}, end: ${event.nativeEvent.selection.end}`,
);
}}
ref={textInputRef}
/>
<Text ref={importantTextNodeRef}>Important text: {text}</Text>
<Text ref={deferredTextNodeRef}>Deferred text: {deferredText}</Text>
</>
);
}
Fantom.runTask(() => {
root.render(<App text={'first 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(<App text={'transition'} />);
});
});
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',
);
});
});
@@ -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<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>,
);
});
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(
<Suspense fallback={<Fallback />}>
<RedSquare />
</Suspense>,
);
});
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(
<Suspense fallback={<Fallback />}>
<GreenSquare />
</Suspense>,
);
});
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 (
<Suspense fallback={<Fallback />}>
{props.color === 'green' ? <GreenSquare /> : <RedSquare />}
</Suspense>
);
}
Fantom.runTask(() => {
root.render(<App color="green" />);
});
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(<App color="red" />);
});
});
// 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"}',
]);
});
});
@@ -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<string> = [];
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 | ReactFragmentInstance>(null);
React.useEffect(() => {
fragmentRef.current?.observeUsing(observer);
const lastRefValue = fragmentRef.current;
return () => {
lastRefValue?.unobserveUsing(observer);
};
}, []);
return (
<View nativeID="parent">
{/* $FlowFixMe oss doesn't have this */}
<React.Fragment ref={fragmentRef}>
<View style={{width: 100, height: 100}} nativeID="childA" />
{showB && (
<View style={{width: 100, height: 100}} nativeID="childB" />
)}
</React.Fragment>
</View>
);
}
Fantom.runTask(() => {
root.render(<Test showB={false} />);
});
expect(logs).toEqual(['show:childA']);
// Reveal child and expect it to be observed and intersecting
logs = [];
Fantom.runTask(() => {
root.render(<Test showB={true} />);
});
expect(logs).toEqual(['show:childB']);
// Hide child and expect it to still be observed, no longer intersecting
logs = [];
Fantom.runTask(() => {
root.render(<Test showB={false} />);
});
expect(logs).toEqual(['hide:childB']);
});
});
});