/** * 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. * * @emails react-core */ 'use strict'; let React = require('react'); let useContext; let ReactNoop; let Scheduler; let gen; let waitForAll; let waitFor; let waitForThrow; let assertConsoleErrorDev; describe('ReactNewContext', () => { beforeEach(() => { jest.resetModules(); React = require('react'); useContext = React.useContext; ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); gen = require('random-seed'); ({ waitForAll, waitFor, waitForThrow, assertConsoleErrorDev, } = require('internal-test-utils')); }); afterEach(() => { jest.restoreAllMocks(); }); function Text(props) { Scheduler.log(props.text); return ; } function span(prop) { return {type: 'span', children: [], prop, hidden: false}; } function readContext(Context) { const dispatcher = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H; return dispatcher.readContext(Context); } // Note: This is based on a similar component we use in www. We can delete // once the extra div wrapper is no longer necessary. function LegacyHiddenDiv({children, mode}) { return ( ); } // We have several ways of reading from context. sharedContextTests runs // a suite of tests for a given context consumer implementation. sharedContextTests('Context.Consumer', Context => Context.Consumer); sharedContextTests( 'useContext inside function component', Context => function Consumer(props) { const contextValue = useContext(Context); const render = props.children; return render(contextValue); }, ); sharedContextTests('useContext inside forwardRef component', Context => React.forwardRef(function Consumer(props, ref) { const contextValue = useContext(Context); const render = props.children; return render(contextValue); }), ); sharedContextTests('useContext inside memoized function component', Context => React.memo(function Consumer(props) { const contextValue = useContext(Context); const render = props.children; return render(contextValue); }), ); sharedContextTests( 'readContext(Context) inside class component', Context => class Consumer extends React.Component { render() { const contextValue = readContext(Context); const render = this.props.children; return render(contextValue); } }, ); sharedContextTests( 'readContext(Context) inside pure class component', Context => class Consumer extends React.PureComponent { render() { const contextValue = readContext(Context); const render = this.props.children; return render(contextValue); } }, ); function sharedContextTests(label, getConsumer) { describe(`reading context with ${label}`, () => { it('simple mount and update', async () => { const Context = React.createContext(1); const Consumer = getConsumer(Context); const Indirection = React.Fragment; function App(props) { return ( {value => } ); } ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); // Update ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); }); it('propagates through shouldComponentUpdate false', async () => { const Context = React.createContext(1); const ContextConsumer = getConsumer(Context); function Provider(props) { Scheduler.log('Provider'); return ( {props.children} ); } function Consumer(props) { Scheduler.log('Consumer'); return ( {value => { Scheduler.log('Consumer render prop'); return ; }} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { Scheduler.log('Indirection'); return this.props.children; } } function App(props) { Scheduler.log('App'); return ( ); } ReactNoop.render(); await waitForAll([ 'App', 'Provider', 'Indirection', 'Indirection', 'Consumer', 'Consumer render prop', ]); expect(ReactNoop).toMatchRenderedOutput(); // Update ReactNoop.render(); await waitForAll(['App', 'Provider', 'Consumer render prop']); expect(ReactNoop).toMatchRenderedOutput(); }); it('consumers bail out if context value is the same', async () => { const Context = React.createContext(1); const ContextConsumer = getConsumer(Context); function Provider(props) { Scheduler.log('Provider'); return ( {props.children} ); } function Consumer(props) { Scheduler.log('Consumer'); return ( {value => { Scheduler.log('Consumer render prop'); return ; }} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { Scheduler.log('Indirection'); return this.props.children; } } function App(props) { Scheduler.log('App'); return ( ); } ReactNoop.render(); await waitForAll([ 'App', 'Provider', 'Indirection', 'Indirection', 'Consumer', 'Consumer render prop', ]); expect(ReactNoop).toMatchRenderedOutput(); // Update with the same context value ReactNoop.render(); await waitForAll([ 'App', 'Provider', // Don't call render prop again ]); expect(ReactNoop).toMatchRenderedOutput(); }); it('nested providers', async () => { const Context = React.createContext(1); const Consumer = getConsumer(Context); function Provider(props) { return ( {contextValue => ( // Multiply previous context value by 2, unless prop overrides {props.children} )} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { return this.props.children; } } function App(props) { return ( {value => } ); } ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); // Update ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); }); it('should provide the correct (default) values to consumers outside of a provider', async () => { const FooContext = React.createContext({value: 'foo-initial'}); const BarContext = React.createContext({value: 'bar-initial'}); const FooConsumer = getConsumer(FooContext); const BarConsumer = getConsumer(BarContext); const Verify = ({actual, expected}) => { expect(expected).toBe(actual); return null; }; ReactNoop.render( <> {({value}) => } {({value}) => ( )} {({value}) => } {({value}) => } , ); await waitForAll([]); }); it('multiple consumers in different branches', async () => { const Context = React.createContext(1); const Consumer = getConsumer(Context); function Provider(props) { return ( {contextValue => ( // Multiply previous context value by 2, unless prop overrides {props.children} )} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { return this.props.children; } } function App(props) { return ( {value => } {value => } ); } ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Update ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Another update ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput( <> , ); }); it('compares context values with Object.is semantics', async () => { const Context = React.createContext(1); const ContextConsumer = getConsumer(Context); function Provider(props) { Scheduler.log('Provider'); return ( {props.children} ); } function Consumer(props) { Scheduler.log('Consumer'); return ( {value => { Scheduler.log('Consumer render prop'); return ; }} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { Scheduler.log('Indirection'); return this.props.children; } } function App(props) { Scheduler.log('App'); return ( ); } ReactNoop.render(); await waitForAll([ 'App', 'Provider', 'Indirection', 'Indirection', 'Consumer', 'Consumer render prop', ]); expect(ReactNoop).toMatchRenderedOutput(); // Update ReactNoop.render(); await waitForAll([ 'App', 'Provider', // Consumer should not re-render again // 'Consumer render prop', ]); expect(ReactNoop).toMatchRenderedOutput(); }); it('context unwinds when interrupted', async () => { const Context = React.createContext('Default'); const ContextConsumer = getConsumer(Context); function Consumer(props) { return ( {value => } ); } function BadRender() { throw new Error('Bad render'); } class ErrorBoundary extends React.Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return null; } return this.props.children; } } function App(props) { return ( <> ); } ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput( // The second provider should use the default value. , ); }); it("does not re-render if there's an update in a child", async () => { const Context = React.createContext(0); const Consumer = getConsumer(Context); let child; class Child extends React.Component { state = {step: 0}; render() { Scheduler.log('Child'); return ( ); } } function App(props) { return ( {value => { Scheduler.log('Consumer render prop'); return (child = inst)} context={value} />; }} ); } // Initial mount ReactNoop.render(); await waitForAll(['Consumer render prop', 'Child']); expect(ReactNoop).toMatchRenderedOutput( , ); child.setState({step: 1}); await waitForAll(['Child']); expect(ReactNoop).toMatchRenderedOutput( , ); }); it('consumer bails out if value is unchanged and something above bailed out', async () => { const Context = React.createContext(0); const Consumer = getConsumer(Context); function renderChildValue(value) { Scheduler.log('Consumer'); return ; } function ChildWithInlineRenderCallback() { Scheduler.log('ChildWithInlineRenderCallback'); // Note: we are intentionally passing an inline arrow. Don't refactor. return {value => renderChildValue(value)}; } function ChildWithCachedRenderCallback() { Scheduler.log('ChildWithCachedRenderCallback'); return {renderChildValue}; } class PureIndirection extends React.PureComponent { render() { Scheduler.log('PureIndirection'); return ( <> ); } } class App extends React.Component { render() { Scheduler.log('App'); return ( ); } } // Initial mount ReactNoop.render(); await waitForAll([ 'App', 'PureIndirection', 'ChildWithInlineRenderCallback', 'Consumer', 'ChildWithCachedRenderCallback', 'Consumer', ]); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Update (bailout) ReactNoop.render(); await waitForAll(['App']); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Update (no bailout) ReactNoop.render(); await waitForAll(['App', 'Consumer', 'Consumer']); expect(ReactNoop).toMatchRenderedOutput( <> , ); }); // @gate enableLegacyHidden it("context consumer doesn't bail out inside hidden subtree", async () => { const Context = React.createContext('dark'); const Consumer = getConsumer(Context); function App({theme}) { return ( {value => } ); } ReactNoop.render(); await waitForAll(['dark']); expect(ReactNoop.getChildrenAsJSX()).toEqual( , ); ReactNoop.render(); await waitForAll(['light']); expect(ReactNoop.getChildrenAsJSX()).toEqual( , ); }); // This is a regression case for https://github.com/facebook/react/issues/12389. it('does not run into an infinite loop', async () => { const Context = React.createContext(null); const Consumer = getConsumer(Context); class App extends React.Component { renderItem(id) { return ( {() => inner} outer ); } renderList() { const list = [1, 2].map(id => this.renderItem(id)); if (this.props.reverse) { list.reverse(); } return list; } render() { return ( {this.renderList()} ); } } ReactNoop.render(); await waitForAll([]); ReactNoop.render(); await waitForAll([]); ReactNoop.render(); await waitForAll([]); }); // This is a regression case for https://github.com/facebook/react/issues/12686 it('does not skip some siblings', async () => { const Context = React.createContext(0); const ContextConsumer = getConsumer(Context); class App extends React.Component { state = { step: 0, }; render() { Scheduler.log('App'); return ( {this.state.step > 0 && } ); } } class StaticContent extends React.PureComponent { render() { return ( <> <> ); } } class Indirection extends React.PureComponent { render() { return ( {value => { Scheduler.log('Consumer'); return ; }} ); } } // Initial mount let inst; ReactNoop.render( (inst = ref)} />); await waitForAll(['App']); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Update the first time inst.setState({step: 1}); await waitForAll(['App', 'Consumer']); expect(ReactNoop).toMatchRenderedOutput( <> , ); // Update the second time inst.setState({step: 2}); await waitForAll(['App', 'Consumer']); expect(ReactNoop).toMatchRenderedOutput( <> , ); }); }); } describe('Context.Provider', () => { it('warns if no value prop provided', async () => { const Context = React.createContext(); ReactNoop.render( , ); await waitForAll([]); assertConsoleErrorDev([ 'The `value` prop is required for the ``. Did you misspell it or forget to pass it?', ]); }); it('warns if multiple renderers concurrently render the same context', async () => { spyOnDev(console, 'error').mockImplementation(() => {}); const Context = React.createContext(0); function Foo(props) { Scheduler.log('Foo'); return null; } function App(props) { return ( ); } React.startTransition(() => { ReactNoop.render(); }); // Render past the Provider, but don't commit yet await waitFor(['Foo']); // Get a new copy of ReactNoop jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; // Render the provider again using a different renderer ReactNoop.render(); await waitForAll(['Foo', 'Foo']); if (__DEV__) { expect(console.error.mock.calls[0][0]).toContain( 'Detected multiple renderers concurrently rendering the same ' + 'context provider. This is currently unsupported', ); } }); it('does not warn if multiple renderers use the same context sequentially', async () => { spyOnDev(console, 'error'); const Context = React.createContext(0); function Foo(props) { Scheduler.log('Foo'); return null; } function App(props) { return ( ); } React.startTransition(() => { ReactNoop.render(); }); await waitForAll(['Foo', 'Foo']); // Get a new copy of ReactNoop jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; // Render the provider again using a different renderer ReactNoop.render(); await waitForAll(['Foo', 'Foo']); if (__DEV__) { expect(console.error).not.toHaveBeenCalled(); } }); it('provider bails out if children and value are unchanged (like sCU)', async () => { const Context = React.createContext(0); function Child() { Scheduler.log('Child'); return ; } const children = ; function App(props) { Scheduler.log('App'); return ( {children} ); } // Initial mount ReactNoop.render(); await waitForAll(['App', 'Child']); expect(ReactNoop).toMatchRenderedOutput(); // Update ReactNoop.render(); await waitForAll([ 'App', // Child does not re-render ]); expect(ReactNoop).toMatchRenderedOutput(); }); // @gate !disableLegacyContext it('provider does not bail out if legacy context changed above', async () => { const Context = React.createContext(0); function Child() { Scheduler.log('Child'); return ; } const children = ; class LegacyProvider extends React.Component { static childContextTypes = { legacyValue: () => {}, }; state = {legacyValue: 1}; getChildContext() { return {legacyValue: this.state.legacyValue}; } render() { Scheduler.log('LegacyProvider'); return this.props.children; } } class App extends React.Component { state = {value: 1}; render() { Scheduler.log('App'); return ( {this.props.children} ); } } const legacyProviderRef = React.createRef(); const appRef = React.createRef(); // Initial mount ReactNoop.render( {children} , ); await waitForAll(['LegacyProvider', 'App', 'Child']); assertConsoleErrorDev([ 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. ' + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + ' in LegacyProvider (at **)', ]); expect(ReactNoop).toMatchRenderedOutput(); // Update App with same value (should bail out) appRef.current.setState({value: 1}); await waitForAll(['App']); expect(ReactNoop).toMatchRenderedOutput(); // Update LegacyProvider (should not bail out) legacyProviderRef.current.setState({value: 1}); await waitForAll(['LegacyProvider', 'App', 'Child']); expect(ReactNoop).toMatchRenderedOutput(); // Update App with same value (should bail out) appRef.current.setState({value: 1}); await waitForAll(['App']); expect(ReactNoop).toMatchRenderedOutput(); }); }); describe('Context.Consumer', () => { it('warns if child is not a function', async () => { spyOnDev(console, 'error').mockImplementation(() => {}); const Context = React.createContext(0); ReactNoop.render(); await waitForThrow('is not a function'); if (__DEV__) { expect(console.error.mock.calls[0][0]).toContain( 'A context consumer was rendered with multiple children, or a child ' + "that isn't a function", ); } }); it('can read other contexts inside consumer render prop', async () => { const FooContext = React.createContext(0); const BarContext = React.createContext(0); function FooAndBar() { return ( {foo => { const bar = readContext(BarContext); return ; }} ); } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { return this.props.children; } } function App(props) { return ( ); } ReactNoop.render(); await waitForAll(['Foo: 1, Bar: 1']); expect(ReactNoop).toMatchRenderedOutput(); // Update foo ReactNoop.render(); await waitForAll(['Foo: 2, Bar: 1']); expect(ReactNoop).toMatchRenderedOutput(); // Update bar ReactNoop.render(); await waitForAll(['Foo: 2, Bar: 2']); expect(ReactNoop).toMatchRenderedOutput(); }); // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. // However, it doesn't bail out from rendering if the component above it re-rendered anyway. // If we bailed out on referential equality, it would be confusing that you // can call this.setState(), but an autobound render callback "blocked" the update. // https://github.com/facebook/react/pull/12470#issuecomment-376917711 it('consumer does not bail out if there were no bailouts above it', async () => { const Context = React.createContext(0); const Consumer = Context.Consumer; class App extends React.Component { state = { text: 'hello', }; renderConsumer = context => { Scheduler.log('App#renderConsumer'); return ; }; render() { Scheduler.log('App'); return ( {this.renderConsumer} ); } } // Initial mount let inst; ReactNoop.render( (inst = ref)} />); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); // Update inst.setState({text: 'goodbye'}); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); }); }); describe('readContext', () => { // Unstable changedBits API was removed. Port this test to context selectors // once that exists. // @gate FIXME it('can read the same context multiple times in the same function', async () => { const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => { let result = 0; if (a.foo !== b.foo) { result |= 0b001; } if (a.bar !== b.bar) { result |= 0b010; } if (a.baz !== b.baz) { result |= 0b100; } return result; }); function Provider(props) { return ( {props.children} ); } function FooAndBar() { const {foo} = readContext(Context, 0b001); const {bar} = readContext(Context, 0b010); return ; } function Baz() { const {baz} = readContext(Context, 0b100); return ; } class Indirection extends React.Component { shouldComponentUpdate() { return false; } render() { return this.props.children; } } function App(props) { return ( ); } ReactNoop.render(); await waitForAll(['Foo: 1, Bar: 1', 'Baz: 1']); expect(ReactNoop).toMatchRenderedOutput([ , , ]); // Update only foo ReactNoop.render(); await waitForAll(['Foo: 2, Bar: 1']); expect(ReactNoop).toMatchRenderedOutput([ , , ]); // Update only bar ReactNoop.render(); await waitForAll(['Foo: 2, Bar: 2']); expect(ReactNoop).toMatchRenderedOutput([ , , ]); // Update only baz ReactNoop.render(); await waitForAll(['Baz: 2']); expect(ReactNoop).toMatchRenderedOutput([ , , ]); }); // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. // However, it doesn't bail out from rendering if the component above it re-rendered anyway. // If we bailed out on referential equality, it would be confusing that you // can call this.setState(), but an autobound render callback "blocked" the update. // https://github.com/facebook/react/pull/12470#issuecomment-376917711 it('does not bail out if there were no bailouts above it', async () => { const Context = React.createContext(0); class Consumer extends React.Component { render() { const contextValue = readContext(Context); return this.props.children(contextValue); } } class App extends React.Component { state = { text: 'hello', }; renderConsumer = context => { Scheduler.log('App#renderConsumer'); return ; }; render() { Scheduler.log('App'); return ( {this.renderConsumer} ); } } // Initial mount let inst; ReactNoop.render( (inst = ref)} />); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); // Update inst.setState({text: 'goodbye'}); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); }); it('warns when reading context inside render phase class setState updater', async () => { const ThemeContext = React.createContext('light'); class Cls extends React.Component { state = {}; render() { this.setState(() => { readContext(ThemeContext); }); return null; } } ReactNoop.render(); await waitForAll([]); assertConsoleErrorDev([ 'Cannot update during an existing state transition (such as within `render`). ' + 'Render methods should be a pure function of props and state.\n' + ' in Cls (at **)', 'Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, ' + 'but not inside Hooks like useReducer() or useMemo().\n' + ' in Cls (at **)', ]); }); }); describe('useContext', () => { it('throws when used in a class component', async () => { const Context = React.createContext(0); class Foo extends React.Component { render() { return useContext(Context); } } ReactNoop.render(); await waitForThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' + ' for one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', ); }); it('warns when passed a consumer', async () => { const Context = React.createContext(0); function Foo() { return useContext(Context.Consumer); } ReactNoop.render(); await waitForAll([]); assertConsoleErrorDev([ 'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' + 'Did you mean to call useContext(Context) instead?\n' + ' in Foo (at **)', ]); }); // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. // However, it doesn't bail out from rendering if the component above it re-rendered anyway. // If we bailed out on referential equality, it would be confusing that you // can call this.setState(), but an autobound render callback "blocked" the update. // https://github.com/facebook/react/pull/12470#issuecomment-376917711 it('does not bail out if there were no bailouts above it', async () => { const Context = React.createContext(0); function Consumer({children}) { const contextValue = useContext(Context); return children(contextValue); } class App extends React.Component { state = { text: 'hello', }; renderConsumer = context => { Scheduler.log('App#renderConsumer'); return ; }; render() { Scheduler.log('App'); return ( {this.renderConsumer} ); } } // Initial mount let inst; ReactNoop.render( (inst = ref)} />); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); // Update inst.setState({text: 'goodbye'}); await waitForAll(['App', 'App#renderConsumer']); expect(ReactNoop).toMatchRenderedOutput(); }); }); it('unwinds after errors in complete phase', async () => { const Context = React.createContext(0); // This is a regression test for stack misalignment // caused by unwinding the context from wrong point. ReactNoop.render( , ); await waitForThrow('Error in host config.'); ReactNoop.render( {value => } , ); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); }); describe('fuzz test', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const FLUSH_ALL = 'FLUSH_ALL'; function flushAll() { return { type: FLUSH_ALL, toString() { return `flushAll()`; }, }; } const FLUSH = 'FLUSH'; function flush(unitsOfWork) { return { type: FLUSH, unitsOfWork, toString() { return `flush(${unitsOfWork})`; }, }; } const UPDATE = 'UPDATE'; function update(key, value) { return { type: UPDATE, key, value, toString() { return `update('${key}', ${value})`; }, }; } function randomInteger(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; } function randomAction() { switch (randomInteger(0, 3)) { case 0: return flushAll(); case 1: return flush(randomInteger(0, 500)); case 2: const key = contextKeys[randomInteger(0, contextKeys.length)]; const value = randomInteger(1, 10); return update(key, value); default: throw new Error('Switch statement should be exhaustive'); } } function randomActions(n) { const actions = []; for (let i = 0; i < n; i++) { actions.push(randomAction()); } return actions; } function ContextSimulator(maxDepth) { const contexts = new Map( contextKeys.map(key => { const Context = React.createContext(0); Context.displayName = 'Context' + key; return [key, Context]; }), ); class ConsumerTree extends React.Component { shouldComponentUpdate() { return false; } render() { Scheduler.log(); if (this.props.depth >= this.props.maxDepth) { return null; } const consumers = [0, 1, 2].map(i => { const randomKey = contextKeys[ this.props.rand.intBetween(0, contextKeys.length - 1) ]; const Context = contexts.get(randomKey); return ( {value => ( <> )} ); }); return consumers; } } function Root(props) { return contextKeys.reduceRight( (children, key) => { const Context = contexts.get(key); const value = props.values[key]; return ( {children} ); }, , ); } const initialValues = contextKeys.reduce( (result, key, i) => ({...result, [key]: i + 1}), {}, ); function assertConsistentTree(expectedValues = {}) { const jsx = ReactNoop.getChildrenAsJSX(); const children = jsx === null ? [] : jsx.props.children; children.forEach(child => { const text = child.props.prop; const key = text[0]; const value = parseInt(text[2], 10); const expectedValue = expectedValues[key]; if (expectedValue === undefined) { // If an expected value was not explicitly passed to this function, // use the first occurrence. expectedValues[key] = value; } else if (value !== expectedValue) { throw new Error( `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${text}`, ); } }); } function simulate(seed, actions) { const rand = gen.create(seed); let finalExpectedValues = initialValues; function updateRoot() { ReactNoop.render( , ); } updateRoot(); actions.forEach(action => { switch (action.type) { case FLUSH_ALL: Scheduler.unstable_flushAllWithoutAsserting(); break; case FLUSH: Scheduler.unstable_flushNumberOfYields(action.unitsOfWork); break; case UPDATE: finalExpectedValues = { ...finalExpectedValues, [action.key]: action.value, }; updateRoot(); break; default: throw new Error('Switch statement should be exhaustive'); } assertConsistentTree(); }); Scheduler.unstable_flushAllWithoutAsserting(); assertConsistentTree(finalExpectedValues); } return {simulate}; } it('hard-coded tests', () => { const {simulate} = ContextSimulator(5); simulate('randomSeed', [flush(3), update('A', 4)]); }); it('generated tests', () => { const {simulate} = ContextSimulator(5); const LIMIT = 100; for (let i = 0; i < LIMIT; i++) { const seed = Math.random().toString(36).slice(2, 7); const actions = randomActions(5); try { simulate(seed, actions); } catch (error) { console.error(` Context fuzz tester error! Copy and paste the following line into the test suite: simulate('${seed}', ${actions.join(', ')}); `); throw error; } } }); }); it('should treat Context as Context.Provider', async () => { const BarContext = React.createContext({value: 'bar-initial'}); expect(BarContext.Provider).toBe(BarContext); function Component() { return ( {({value}) => } ); } ReactNoop.render(); await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput(); }); });