mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
273c2d842d
commit
b2d9f5f280
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-236
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
-235
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user