Add xplat test variants (#29734)

## Overview

We didn't have any tests that ran in persistent mode with the xplat
feature flags (for either variant).

As a result, invalid test gating like in
https://github.com/facebook/react/pull/29664 were not caught.

This PR adds test flavors for `ReactFeatureFlag-native-fb.js` in both
variants.
This commit is contained in:
Ricky 2024-06-04 13:07:29 -04:00 committed by GitHub
parent 9185b9b1e4
commit eabb681535
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 247 additions and 144 deletions

View File

@ -507,6 +507,10 @@ workflows:
- "-r=www-modern --env=production --variant=false"
- "-r=www-modern --env=development --variant=true"
- "-r=www-modern --env=production --variant=true"
- "-r=xplat --env=development --variant=false"
- "-r=xplat --env=development --variant=true"
- "-r=xplat --env=production --variant=false"
- "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
- '-r=stable --env=development --persistent'
@ -552,6 +556,12 @@ workflows:
# - "-r=www-modern --env=development --variant=true"
# - "-r=www-modern --env=production --variant=true"
# TODO: Update test config to support xplat build tests
# - "-r=xplat --env=development --variant=false"
# - "-r=xplat --env=development --variant=true"
# - "-r=xplat --env=production --variant=false"
# - "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
- download_base_build_for_sizebot:
filters:

View File

@ -1377,113 +1377,118 @@ describe('ResponderEventPlugin', () => {
expect(ResponderEventPlugin._getResponder()).toBe(null);
});
it('should determine the first common ancestor correctly', async () => {
// This test was moved here from the ReactTreeTraversal test since only the
// ResponderEventPlugin uses `getLowestCommonAncestor`
const React = require('react');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
const getLowestCommonAncestor =
require('react-native-renderer/src/legacy-events/ResponderEventPlugin').getLowestCommonAncestor;
// This works by accident and will likely break in the future.
const ReactDOMComponentTree = require('react-dom-bindings/src/client/ReactDOMComponentTree');
it(
'should determine the first common ancestor correctly',
async () => {
// This test was moved here from the ReactTreeTraversal test since only the
// ResponderEventPlugin uses `getLowestCommonAncestor`
const React = require('react');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
const getLowestCommonAncestor =
require('react-native-renderer/src/legacy-events/ResponderEventPlugin').getLowestCommonAncestor;
// This works by accident and will likely break in the future.
const ReactDOMComponentTree = require('react-dom-bindings/src/client/ReactDOMComponentTree');
class ChildComponent extends React.Component {
divRef = React.createRef();
div1Ref = React.createRef();
div2Ref = React.createRef();
class ChildComponent extends React.Component {
divRef = React.createRef();
div1Ref = React.createRef();
div2Ref = React.createRef();
render() {
return (
<div ref={this.divRef} id={this.props.id + '__DIV'}>
<div ref={this.div1Ref} id={this.props.id + '__DIV_1'} />
<div ref={this.div2Ref} id={this.props.id + '__DIV_2'} />
</div>
);
}
}
class ParentComponent extends React.Component {
pRef = React.createRef();
p_P1Ref = React.createRef();
p_P1_C1Ref = React.createRef();
p_P1_C2Ref = React.createRef();
p_OneOffRef = React.createRef();
render() {
return (
<div ref={this.pRef} id="P">
<div ref={this.p_P1Ref} id="P_P1">
<ChildComponent ref={this.p_P1_C1Ref} id="P_P1_C1" />
<ChildComponent ref={this.p_P1_C2Ref} id="P_P1_C2" />
render() {
return (
<div ref={this.divRef} id={this.props.id + '__DIV'}>
<div ref={this.div1Ref} id={this.props.id + '__DIV_1'} />
<div ref={this.div2Ref} id={this.props.id + '__DIV_2'} />
</div>
<div ref={this.p_OneOffRef} id="P_OneOff" />
</div>
);
}
}
class ParentComponent extends React.Component {
pRef = React.createRef();
p_P1Ref = React.createRef();
p_P1_C1Ref = React.createRef();
p_P1_C2Ref = React.createRef();
p_OneOffRef = React.createRef();
render() {
return (
<div ref={this.pRef} id="P">
<div ref={this.p_P1Ref} id="P_P1">
<ChildComponent ref={this.p_P1_C1Ref} id="P_P1_C1" />
<ChildComponent ref={this.p_P1_C2Ref} id="P_P1_C2" />
</div>
<div ref={this.p_OneOffRef} id="P_OneOff" />
</div>
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let parent;
await act(() => {
root.render(<ParentComponent ref={current => (parent = current)} />);
});
const ancestors = [
// Common ancestor with self is self.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.div1Ref.current,
com: parent.p_P1_C1Ref.current.div1Ref.current,
},
// Common ancestor with self is self - even if topmost DOM.
{
one: parent.pRef.current,
two: parent.pRef.current,
com: parent.pRef.current,
},
// Siblings
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.div2Ref.current,
com: parent.p_P1_C1Ref.current.divRef.current,
},
// Common ancestor with parent is the parent.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.divRef.current,
com: parent.p_P1_C1Ref.current.divRef.current,
},
// Common ancestor with grandparent is the grandparent.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1Ref.current,
com: parent.p_P1Ref.current,
},
// Grandparent across subcomponent boundaries.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C2Ref.current.div1Ref.current,
com: parent.p_P1Ref.current,
},
// Something deep with something one-off.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_OneOffRef.current,
com: parent.pRef.current,
},
];
let i;
for (i = 0; i < ancestors.length; i++) {
const plan = ancestors[i];
const firstCommon = getLowestCommonAncestor(
ReactDOMComponentTree.getInstanceFromNode(plan.one),
ReactDOMComponentTree.getInstanceFromNode(plan.two),
);
expect(firstCommon).toBe(
ReactDOMComponentTree.getInstanceFromNode(plan.com),
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let parent;
await act(() => {
root.render(<ParentComponent ref={current => (parent = current)} />);
});
const ancestors = [
// Common ancestor with self is self.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.div1Ref.current,
com: parent.p_P1_C1Ref.current.div1Ref.current,
},
// Common ancestor with self is self - even if topmost DOM.
{
one: parent.pRef.current,
two: parent.pRef.current,
com: parent.pRef.current,
},
// Siblings
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.div2Ref.current,
com: parent.p_P1_C1Ref.current.divRef.current,
},
// Common ancestor with parent is the parent.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C1Ref.current.divRef.current,
com: parent.p_P1_C1Ref.current.divRef.current,
},
// Common ancestor with grandparent is the grandparent.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1Ref.current,
com: parent.p_P1Ref.current,
},
// Grandparent across subcomponent boundaries.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_P1_C2Ref.current.div1Ref.current,
com: parent.p_P1Ref.current,
},
// Something deep with something one-off.
{
one: parent.p_P1_C1Ref.current.div1Ref.current,
two: parent.p_OneOffRef.current,
com: parent.pRef.current,
},
];
let i;
for (i = 0; i < ancestors.length; i++) {
const plan = ancestors[i];
const firstCommon = getLowestCommonAncestor(
ReactDOMComponentTree.getInstanceFromNode(plan.one),
ReactDOMComponentTree.getInstanceFromNode(plan.two),
);
expect(firstCommon).toBe(
ReactDOMComponentTree.getInstanceFromNode(plan.com),
);
}
});
},
// TODO: this is a long running test, we should speed it up.
60 * 1000,
);
});

View File

@ -118,7 +118,7 @@ describe('Activity', () => {
);
});
// @gate www && !disableLegacyMode
// @gate enableLegacyHidden && !disableLegacyMode
it('does not defer in legacy mode', async () => {
let setState;
function Foo() {
@ -163,7 +163,7 @@ describe('Activity', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it('does defer in concurrent mode', async () => {
let setState;
function Foo() {

View File

@ -140,7 +140,7 @@ describe('Activity Suspense', () => {
);
});
// @gate www
// @gate enableLegacyHidden
test('LegacyHidden does not handle suspense', async () => {
const root = ReactNoop.createRoot();
@ -174,7 +174,7 @@ describe('Activity Suspense', () => {
);
});
// @gate experimental || www
// @gate enableActivity
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
@ -319,7 +319,7 @@ describe('Activity Suspense', () => {
);
});
// @gate experimental || www
// @gate enableActivity
test('update that suspends inside hidden tree', async () => {
let setText;
function Child() {
@ -352,7 +352,7 @@ describe('Activity Suspense', () => {
});
});
// @gate experimental || www
// @gate enableActivity
test('updates at multiple priorities that suspend inside hidden tree', async () => {
let setText;
let setStep;

View File

@ -550,7 +550,7 @@ describe('ReactLazyContextPropagation', () => {
expect(root).toMatchRenderedOutput('BB');
});
// @gate www
// @gate enableLegacyCache && enableLegacyHidden
test('context is propagated through offscreen trees', async () => {
const LegacyHidden = React.unstable_LegacyHidden;
@ -596,7 +596,7 @@ describe('ReactLazyContextPropagation', () => {
expect(root).toMatchRenderedOutput('BB');
});
// @gate www
// @gate enableLegacyCache && enableLegacyHidden
test('multiple contexts are propagated across through offscreen trees', async () => {
// Same as previous test, but with multiple context providers
const LegacyHidden = React.unstable_LegacyHidden;
@ -822,7 +822,7 @@ describe('ReactLazyContextPropagation', () => {
expect(root).toMatchRenderedOutput('BB');
});
// @gate www
// @gate enableLegacyCache && enableLegacyHidden
test('nested bailouts through offscreen trees', async () => {
// Lazy context propagation will stop propagating when it hits the first
// match. If we bail out again inside that tree, we must resume propagating.

View File

@ -239,7 +239,7 @@ describe('ReactIncremental', () => {
expect(inst.state).toEqual({text: 'bar', text2: 'baz'});
});
// @gate www
// @gate enableLegacyHidden
it('can deprioritize unfinished work and resume it later', async () => {
function Bar(props) {
Scheduler.log('Bar');
@ -279,7 +279,7 @@ describe('ReactIncremental', () => {
await waitForAll(['Middle', 'Middle']);
});
// @gate www
// @gate enableLegacyHidden
it('can deprioritize a tree from without dropping work', async () => {
function Bar(props) {
Scheduler.log('Bar');
@ -1864,8 +1864,7 @@ describe('ReactIncremental', () => {
]);
});
// @gate www
// @gate !disableLegacyContext
// @gate enableLegacyHidden && !disableLegacyContext
it('provides context when reusing work', async () => {
class Intl extends React.Component {
static childContextTypes = {

View File

@ -289,7 +289,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it('does not include offscreen work when retrying after an error', async () => {
function App(props) {
if (props.isBroken) {

View File

@ -481,7 +481,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it('preserves a previously rendered node when deprioritized', async () => {
function Middle(props) {
Scheduler.log('Middle');
@ -530,7 +530,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it('can reuse side-effects after being preempted', async () => {
function Bar(props) {
Scheduler.log('Bar');
@ -610,7 +610,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', async () => {
class Bar extends React.Component {
shouldComponentUpdate(nextProps) {
@ -733,7 +733,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={3} />);
});
// @gate www
// @gate enableLegacyHidden
it('updates a child even though the old props is empty', async () => {
function Foo(props) {
return (
@ -984,7 +984,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar']);
});
// @gate www
// @gate enableLegacyHidden
it('deprioritizes setStates that happens within a deprioritized tree', async () => {
const barInstances = [];

View File

@ -699,7 +699,7 @@ describe('ReactNewContext', () => {
);
});
// @gate www
// @gate enableLegacyHidden
it("context consumer doesn't bail out inside hidden subtree", async () => {
const Context = React.createContext('dark');
const Consumer = getConsumer(Context);

View File

@ -131,7 +131,7 @@ describe('ReactSchedulerIntegration', () => {
await waitForAll(['D', 'E']);
});
// @gate www
// @gate enableLegacyHidden
it('idle updates are not blocked by offscreen work', async () => {
function Text({text}) {
Scheduler.log(text);

View File

@ -41,7 +41,7 @@ describe('ReactScope', () => {
container = null;
});
// @gate www
// @gate enableScopeAPI
it('DO_NOT_USE_queryAllNodes() works as intended', async () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_Scope;
@ -86,7 +86,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
// @gate www
// @gate enableScopeAPI
it('DO_NOT_USE_queryAllNodes() provides the correct host instance', async () => {
const testScopeQuery = (type, props) => type === 'div';
const TestScope = React.unstable_Scope;
@ -143,7 +143,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
// @gate www
// @gate enableScopeAPI
it('DO_NOT_USE_queryFirstNode() works as intended', async () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_Scope;
@ -188,7 +188,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
// @gate www
// @gate enableScopeAPI
it('containsNode() works as intended', async () => {
const TestScope = React.unstable_Scope;
const scopeRef = React.createRef();
@ -248,7 +248,7 @@ describe('ReactScope', () => {
expect(scopeRef.current.containsNode(emRef.current)).toBe(false);
});
// @gate www
// @gate enableScopeAPI
it('scopes support server-side rendering and hydration', async () => {
const TestScope = React.unstable_Scope;
const scopeRef = React.createRef();
@ -281,7 +281,7 @@ describe('ReactScope', () => {
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
});
// @gate www
// @gate enableScopeAPI
it('getChildContextValues() works as intended', async () => {
const TestContext = React.createContext();
const TestScope = React.unstable_Scope;
@ -320,7 +320,7 @@ describe('ReactScope', () => {
expect(scopeRef.current).toBe(null);
});
// @gate www
// @gate enableScopeAPI
it('correctly works with suspended boundaries that are hydrated', async () => {
let suspend = false;
let resolve;
@ -392,7 +392,7 @@ describe('ReactScope', () => {
ReactTestRenderer = require('react-test-renderer');
});
// @gate www
// @gate enableScopeAPI
it('DO_NOT_USE_queryAllNodes() works as intended', async () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_Scope;
@ -434,7 +434,7 @@ describe('ReactScope', () => {
expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
});
// @gate www
// @gate enableScopeAPI
it('DO_NOT_USE_queryFirstNode() works as intended', async () => {
const testScopeQuery = (type, props) => true;
const TestScope = React.unstable_Scope;
@ -477,7 +477,7 @@ describe('ReactScope', () => {
expect(node).toEqual(aRef.current);
});
// @gate www
// @gate enableScopeAPI
it('containsNode() works as intended', async () => {
const TestScope = React.unstable_Scope;
const scopeRef = React.createRef();

View File

@ -130,7 +130,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
const resolveText = resolveMostRecentTextCache;
// @gate www && !disableLegacyMode
// @gate enableLegacyCache && !disableLegacyMode
it('regression: false positive for legacy suspense', async () => {
const Child = ({text}) => {
// If text hasn't resolved, this will throw and exit before the passive

View File

@ -2441,7 +2441,7 @@ describe('ReactFresh', () => {
}
});
// @gate www && __DEV__
// @gate enableLegacyHidden && __DEV__
it('can hot reload offscreen components', async () => {
const AppV1 = prepare(() => {
function Hello() {

View File

@ -170,6 +170,17 @@ describe(`onRender`, () => {
'read current time',
'read current time',
]);
} else if (gate(flags => !flags.allowConcurrentByDefault)) {
assertLog([
'read current time',
'read current time',
'read current time',
'read current time',
'read current time',
'read current time',
'read current time',
// TODO: why is there one less in this case?
]);
} else {
assertLog([
'read current time',

View File

@ -60,6 +60,7 @@ function getTestFlags() {
const schedulerFeatureFlags = require('scheduler/src/SchedulerFeatureFlags');
const www = global.__WWW__ === true;
const xplat = global.__XPLAT__ === true;
const releaseChannel = www
? __EXPERIMENTAL__
? 'modern'
@ -79,8 +80,8 @@ function getTestFlags() {
www,
// These aren't flags, just a useful aliases for tests.
enableActivity: releaseChannel === 'experimental' || www,
enableSuspenseList: releaseChannel === 'experimental' || www,
enableActivity: releaseChannel === 'experimental' || www || xplat,
enableSuspenseList: releaseChannel === 'experimental' || www || xplat,
enableLegacyHidden: www,
// This flag is used to determine whether we should run Fizz tests using

View File

@ -0,0 +1,30 @@
'use strict';
const baseConfig = require('./config.base');
module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-shared',
'ReactIncrementalPerf',
'ReactIncrementalUpdatesMinimalism',
'ReactIncrementalTriangle',
'ReactIncrementalReflection',
'forwardRef',
],
// RN configs should not run react-dom tests.
// There are many other tests that use react-dom
// and for those we will use the www entrypoint,
// but those tests should be migrated to Noop renderer.
testPathIgnorePatterns: [
'node_modules',
'packages/react-dom',
'packages/react-server-dom-webpack',
],
setupFiles: [
...baseConfig.setupFiles,
require.resolve('./setupTests.xplat.js'),
require.resolve('./setupHostConfigs.js'),
],
});

View File

@ -9,6 +9,7 @@ const semver = require('semver');
const ossConfig = './scripts/jest/config.source.js';
const wwwConfig = './scripts/jest/config.source-www.js';
const xplatConfig = './scripts/jest/config.source-xplat.js';
const devToolsConfig = './scripts/jest/config.build-devtools.js';
// TODO: These configs are separate but should be rolled into the configs above
@ -46,7 +47,7 @@ const argv = yargs
requiresArg: true,
type: 'string',
default: 'experimental',
choices: ['experimental', 'stable', 'www-classic', 'www-modern'],
choices: ['experimental', 'stable', 'www-classic', 'www-modern', 'xplat'],
},
env: {
alias: 'e',
@ -124,6 +125,10 @@ function isWWWConfig() {
);
}
function isXplatConfig() {
return argv.releaseChannel === 'xplat' && argv.project !== 'devtools';
}
function isOSSConfig() {
return (
argv.releaseChannel === 'stable' || argv.releaseChannel === 'experimental'
@ -189,7 +194,7 @@ function validateOptions() {
}
}
if (isWWWConfig()) {
if (isWWWConfig() || isXplatConfig()) {
if (argv.variant === undefined) {
// Turn internal experiments on by default
argv.variant = true;
@ -224,6 +229,13 @@ function validateOptions() {
success = false;
}
if (argv.build && isXplatConfig()) {
logError(
'Build targets are only not supported for xplat release channels. Update these options to continue.'
);
success = false;
}
if (argv.env && argv.env !== 'production' && argv.prod) {
logError(
'Build type does not match --prod. Update these options to continue.'
@ -277,6 +289,8 @@ function getCommandArgs() {
args.push(persistentConfig);
} else if (isWWWConfig()) {
args.push(wwwConfig);
} else if (isXplatConfig()) {
args.push(xplatConfig);
} else if (isOSSConfig()) {
args.push(ossConfig);
} else {

View File

@ -77,7 +77,7 @@ function mockReact() {
jest.mock('react', () => {
const resolvedEntryPoint = resolveEntryFork(
require.resolve('react'),
global.__WWW__
global.__WWW__ || global.__XPLAT__
);
return jest.requireActual(resolvedEntryPoint);
});
@ -100,7 +100,7 @@ jest.mock('react/react.react-server', () => {
});
const resolvedEntryPoint = resolveEntryFork(
require.resolve('react/src/ReactServer'),
global.__WWW__
global.__WWW__ || global.__XPLAT__
);
return jest.requireActual(resolvedEntryPoint);
});
@ -198,7 +198,7 @@ inlinedHostConfigs.forEach(rendererInfo => {
mockAllConfigs(rendererInfo);
const resolvedEntryPoint = resolveEntryFork(
require.resolve(entryPoint),
global.__WWW__
global.__WWW__ || global.__XPLAT__
);
return jest.requireActual(resolvedEntryPoint);
});

View File

@ -0,0 +1,33 @@
'use strict';
jest.mock('shared/ReactFeatureFlags', () => {
jest.mock(
'ReactNativeInternalFeatureFlags',
() =>
jest.requireActual('shared/forks/ReactFeatureFlags.native-fb-dynamic.js'),
{virtual: true}
);
const actual = jest.requireActual(
'shared/forks/ReactFeatureFlags.native-fb.js'
);
// Lots of tests use these, but we don't want to expose it to RN.
// Ideally, tests for xplat wouldn't use react-dom, but many of our tests do.
// Since the xplat tests run with the www entry points, some of these flags
// need to be set to the www value for the entrypoint, otherwise gating would
// fail due to the tests passing. Ideally, the www entry points for these APIs
// would be gated, and then these would fail correctly.
actual.enableLegacyCache = true;
actual.enableLegacyHidden = true;
actual.enableScopeAPI = true;
actual.enableTaint = false;
return actual;
});
jest.mock('react-noop-renderer', () =>
jest.requireActual('react-noop-renderer/persistent')
);
global.__PERSISTENT__ = true;
global.__XPLAT__ = true;