Codemod tests to `it.experimental` (#17243)

`it.experimental` marks that a test only works in Experimental builds.

It also asserts that a test does *not* work in the stable builds. The
main benefit is that we're less likely to accidentally expose an
experimental API before we intend. It also forces us to un- mark an
experimental test once it become stable.
This commit is contained in:
Andrew Clark 2019-11-01 10:20:08 -07:00 committed by GitHub
parent a1ff9fd7bb
commit 6dc2734b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1028 additions and 1031 deletions

View File

@ -47,255 +47,254 @@ describe('ReactDOMFiberAsync', () => {
expect(ops).toEqual(['Hi', 'Bye']);
});
if (__EXPERIMENTAL__) {
describe('concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
it('flushSync batches sync updates and flushes them at the end of the batch', () => {
let ops = [];
let instance;
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
}
ReactDOM.render(<Component />, container);
instance.push('A');
expect(ops).toEqual(['A']);
expect(container.textContent).toEqual('A');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('A');
expect(ops).toEqual(['A']);
});
expect(container.textContent).toEqual('ABC');
expect(ops).toEqual(['A', 'ABC']);
instance.push('D');
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABC', 'ABCD']);
});
it('flushSync flushes updates even if nested inside another flushSync', () => {
let ops = [];
let instance;
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
}
ReactDOM.render(<Component />, container);
instance.push('A');
expect(ops).toEqual(['A']);
expect(container.textContent).toEqual('A');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('A');
expect(ops).toEqual(['A']);
ReactDOM.flushSync(() => {
instance.push('D');
});
// The nested flushSync caused everything to flush.
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABCD']);
});
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABCD']);
});
it('does not perform deferred updates synchronously', () => {
let inputRef = React.createRef();
let asyncValueRef = React.createRef();
let syncValueRef = React.createRef();
it('flushSync throws if already performing work', () => {
class Component extends React.Component {
componentDidUpdate() {
ReactDOM.flushSync(() => {});
}
render() {
return null;
}
}
class Counter extends React.Component {
state = {asyncValue: '', syncValue: ''};
// Initial mount
ReactDOM.render(<Component />, container);
// Update
expect(() => ReactDOM.render(<Component />, container)).toThrow(
'flushSync was called from inside a lifecycle method',
);
});
handleChange = e => {
const nextValue = e.target.value;
requestIdleCallback(() => {
this.setState({
asyncValue: nextValue,
});
// It should not be flushed yet.
expect(asyncValueRef.current.textContent).toBe('');
});
describe('concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it.experimental('does not perform deferred updates synchronously', () => {
let inputRef = React.createRef();
let asyncValueRef = React.createRef();
let syncValueRef = React.createRef();
class Counter extends React.Component {
state = {asyncValue: '', syncValue: ''};
handleChange = e => {
const nextValue = e.target.value;
requestIdleCallback(() => {
this.setState({
syncValue: nextValue,
asyncValue: nextValue,
});
};
render() {
return (
<div>
<input
ref={inputRef}
onChange={this.handleChange}
defaultValue=""
/>
<p ref={asyncValueRef}>{this.state.asyncValue}</p>
<p ref={syncValueRef}>{this.state.syncValue}</p>
</div>
);
}
}
const root = ReactDOM.createRoot(container);
root.render(<Counter />);
Scheduler.unstable_flushAll();
expect(asyncValueRef.current.textContent).toBe('');
expect(syncValueRef.current.textContent).toBe('');
setUntrackedInputValue.call(inputRef.current, 'hello');
inputRef.current.dispatchEvent(
new MouseEvent('input', {bubbles: true}),
);
// Should only flush non-deferred update.
expect(asyncValueRef.current.textContent).toBe('');
expect(syncValueRef.current.textContent).toBe('hello');
// Should flush both updates now.
jest.runAllTimers();
Scheduler.unstable_flushAll();
expect(asyncValueRef.current.textContent).toBe('hello');
expect(syncValueRef.current.textContent).toBe('hello');
});
it('top-level updates are concurrent', () => {
const root = ReactDOM.createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
root.render(<div>Bye</div>);
expect(container.textContent).toEqual('Hi');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Bye');
});
it('deep updates (setState) are concurrent', () => {
let instance;
class Component extends React.Component {
state = {step: 0};
render() {
instance = this;
return <div>{this.state.step}</div>;
}
}
const root = ReactDOM.createRoot(container);
root.render(<Component />);
expect(container.textContent).toEqual('');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('0');
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('1');
});
it('flushSync batches sync updates and flushes them at the end of the batch', () => {
let ops = [];
let instance;
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
}
ReactDOM.render(<Component />, container);
instance.push('A');
expect(ops).toEqual(['A']);
expect(container.textContent).toEqual('A');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('A');
expect(ops).toEqual(['A']);
});
expect(container.textContent).toEqual('ABC');
expect(ops).toEqual(['A', 'ABC']);
instance.push('D');
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABC', 'ABCD']);
});
it('flushSync flushes updates even if nested inside another flushSync', () => {
let ops = [];
let instance;
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
}
ReactDOM.render(<Component />, container);
instance.push('A');
expect(ops).toEqual(['A']);
expect(container.textContent).toEqual('A');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('A');
expect(ops).toEqual(['A']);
ReactDOM.flushSync(() => {
instance.push('D');
// It should not be flushed yet.
expect(asyncValueRef.current.textContent).toBe('');
});
// The nested flushSync caused everything to flush.
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABCD']);
});
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['A', 'ABCD']);
});
this.setState({
syncValue: nextValue,
});
};
it('flushSync throws if already performing work', () => {
class Component extends React.Component {
componentDidUpdate() {
ReactDOM.flushSync(() => {});
}
render() {
return null;
}
render() {
return (
<div>
<input
ref={inputRef}
onChange={this.handleChange}
defaultValue=""
/>
<p ref={asyncValueRef}>{this.state.asyncValue}</p>
<p ref={syncValueRef}>{this.state.syncValue}</p>
</div>
);
}
}
const root = ReactDOM.createRoot(container);
root.render(<Counter />);
Scheduler.unstable_flushAll();
expect(asyncValueRef.current.textContent).toBe('');
expect(syncValueRef.current.textContent).toBe('');
// Initial mount
ReactDOM.render(<Component />, container);
// Update
expect(() => ReactDOM.render(<Component />, container)).toThrow(
'flushSync was called from inside a lifecycle method',
);
});
setUntrackedInputValue.call(inputRef.current, 'hello');
inputRef.current.dispatchEvent(new MouseEvent('input', {bubbles: true}));
// Should only flush non-deferred update.
expect(asyncValueRef.current.textContent).toBe('');
expect(syncValueRef.current.textContent).toBe('hello');
it('flushSync flushes updates before end of the tick', () => {
let ops = [];
let instance;
// Should flush both updates now.
jest.runAllTimers();
Scheduler.unstable_flushAll();
expect(asyncValueRef.current.textContent).toBe('hello');
expect(syncValueRef.current.textContent).toBe('hello');
});
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
it.experimental('top-level updates are concurrent', () => {
const root = ReactDOM.createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
root.render(<div>Bye</div>);
expect(container.textContent).toEqual('Hi');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Bye');
});
it.experimental('deep updates (setState) are concurrent', () => {
let instance;
class Component extends React.Component {
state = {step: 0};
render() {
instance = this;
return <div>{this.state.step}</div>;
}
}
const root = ReactDOM.createRoot(container);
root.render(<Component />);
Scheduler.unstable_flushAll();
const root = ReactDOM.createRoot(container);
root.render(<Component />);
expect(container.textContent).toEqual('');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('0');
// Updates are async by default
instance.push('A');
expect(ops).toEqual([]);
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('1');
});
it.experimental('flushSync flushes updates before end of the tick', () => {
let ops = [];
let instance;
class Component extends React.Component {
state = {text: ''};
push(val) {
this.setState(state => ({text: state.text + val}));
}
componentDidUpdate() {
ops.push(this.state.text);
}
render() {
instance = this;
return <span>{this.state.text}</span>;
}
}
const root = ReactDOM.createRoot(container);
root.render(<Component />);
Scheduler.unstable_flushAll();
// Updates are async by default
instance.push('A');
expect(ops).toEqual([]);
expect(container.textContent).toEqual('');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('');
ReactDOM.flushSync(() => {
instance.push('B');
instance.push('C');
// Not flushed yet
expect(container.textContent).toEqual('');
expect(ops).toEqual([]);
});
// Only the active updates have flushed
expect(container.textContent).toEqual('BC');
expect(ops).toEqual(['BC']);
instance.push('D');
expect(container.textContent).toEqual('BC');
expect(ops).toEqual(['BC']);
// Flush the async updates
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['BC', 'ABCD']);
expect(ops).toEqual([]);
});
// Only the active updates have flushed
expect(container.textContent).toEqual('BC');
expect(ops).toEqual(['BC']);
it('flushControlled flushes updates before yielding to browser', () => {
instance.push('D');
expect(container.textContent).toEqual('BC');
expect(ops).toEqual(['BC']);
// Flush the async updates
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['BC', 'ABCD']);
});
it.experimental(
'flushControlled flushes updates before yielding to browser',
() => {
let inst;
class Counter extends React.Component {
state = {counter: 0};
@ -332,9 +331,12 @@ describe('ReactDOMFiberAsync', () => {
'end of outer flush: 1',
'after outer flush: 3',
]);
});
},
);
it('flushControlled does not flush until end of outermost batchedUpdates', () => {
it.experimental(
'flushControlled does not flush until end of outermost batchedUpdates',
() => {
let inst;
class Counter extends React.Component {
state = {counter: 0};
@ -362,32 +364,35 @@ describe('ReactDOMFiberAsync', () => {
'end of batchedUpdates fn: 0',
'after batchedUpdates: 2',
]);
});
},
);
it('flushControlled returns nothing', () => {
// In the future, we may want to return a thenable "work" object.
let inst;
class Counter extends React.Component {
state = {counter: 0};
increment = () =>
this.setState(state => ({counter: state.counter + 1}));
render() {
inst = this;
return this.state.counter;
}
it.experimental('flushControlled returns nothing', () => {
// In the future, we may want to return a thenable "work" object.
let inst;
class Counter extends React.Component {
state = {counter: 0};
increment = () =>
this.setState(state => ({counter: state.counter + 1}));
render() {
inst = this;
return this.state.counter;
}
ReactDOM.render(<Counter />, container);
expect(container.textContent).toEqual('0');
}
ReactDOM.render(<Counter />, container);
expect(container.textContent).toEqual('0');
const returnValue = ReactDOM.unstable_flushControlled(() => {
inst.increment();
return 'something';
});
expect(container.textContent).toEqual('1');
expect(returnValue).toBe(undefined);
const returnValue = ReactDOM.unstable_flushControlled(() => {
inst.increment();
return 'something';
});
expect(container.textContent).toEqual('1');
expect(returnValue).toBe(undefined);
});
it('ignores discrete events on a pending removed element', () => {
it.experimental(
'ignores discrete events on a pending removed element',
() => {
const disableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
@ -446,9 +451,12 @@ describe('ReactDOMFiberAsync', () => {
expect(formSubmitted).toBe(false);
expect(submitButtonRef.current).toBe(null);
});
},
);
it('ignores discrete events on a pending removed event listener', () => {
it.experimental(
'ignores discrete events on a pending removed event listener',
() => {
const disableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
@ -512,9 +520,12 @@ describe('ReactDOMFiberAsync', () => {
// Therefore the form should never have been submitted.
expect(formSubmitted).toBe(false);
});
},
);
it('uses the newest discrete events on a pending changed event listener', () => {
it.experimental(
'uses the newest discrete events on a pending changed event listener',
() => {
const enableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
@ -572,33 +583,33 @@ describe('ReactDOMFiberAsync', () => {
// Therefore the form should have been submitted.
expect(formSubmitted).toBe(true);
});
},
);
});
describe('createBlockingRoot', () => {
it.experimental('updates flush without yielding in the next event', () => {
const root = ReactDOM.createBlockingRoot(container);
function Text(props) {
Scheduler.unstable_yieldValue(props.text);
return props.text;
}
root.render(
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
</>,
);
// Nothing should have rendered yet
expect(container.textContent).toEqual('');
// Everything should render immediately in the next event
expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
expect(container.textContent).toEqual('ABC');
});
describe('createBlockingRoot', () => {
it('updates flush without yielding in the next event', () => {
const root = ReactDOM.createBlockingRoot(container);
function Text(props) {
Scheduler.unstable_yieldValue(props.text);
return props.text;
}
root.render(
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
</>,
);
// Nothing should have rendered yet
expect(container.textContent).toEqual('');
// Everything should render immediately in the next event
expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
expect(container.textContent).toEqual('ABC');
});
});
}
});
});

View File

@ -105,8 +105,9 @@ describe('ReactDOMHooks', () => {
expect(labelRef.current.innerHTML).toBe('abc');
});
if (__EXPERIMENTAL__) {
it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => {
it.experimental(
'should not bail out when an update is scheduled from within an event handler in Concurrent Mode',
() => {
const {createRef, useCallback, useState} = React;
const Example = ({inputRef, labelRef}) => {
@ -139,6 +140,6 @@ describe('ReactDOMHooks', () => {
Scheduler.unstable_flushAll();
expect(labelRef.current.innerHTML).toBe('abc');
});
}
},
);
});

View File

@ -500,8 +500,9 @@ describe('ReactDOMServerHydration', () => {
expect(element.textContent).toBe('Hello world');
});
if (__EXPERIMENTAL__) {
it('does not re-enter hydration after committing the first one', () => {
it.experimental(
'does not re-enter hydration after committing the first one',
() => {
let finalHTML = ReactDOMServer.renderToString(<div />);
let container = document.createElement('div');
container.innerHTML = finalHTML;
@ -514,8 +515,8 @@ describe('ReactDOMServerHydration', () => {
// warnings.
root.render(<div />);
Scheduler.unstable_flushAll();
});
}
},
);
it('Suspense + hydration in legacy mode', () => {
const element = document.createElement('div');

View File

@ -125,31 +125,27 @@ describe('ReactTestUtils.act()', () => {
]);
});
if (__EXPERIMENTAL__) {
it('warns in blocking mode', () => {
expect(() => {
const root = ReactDOM.createBlockingRoot(
document.createElement('div'),
);
root.render(<App />);
Scheduler.unstable_flushAll();
}).toWarnDev([
'An update to App ran an effect, but was not wrapped in act(...)',
'An update to App ran an effect, but was not wrapped in act(...)',
]);
});
it.experimental('warns in blocking mode', () => {
expect(() => {
const root = ReactDOM.createBlockingRoot(document.createElement('div'));
root.render(<App />);
Scheduler.unstable_flushAll();
}).toWarnDev([
'An update to App ran an effect, but was not wrapped in act(...)',
'An update to App ran an effect, but was not wrapped in act(...)',
]);
});
it('warns in concurrent mode', () => {
expect(() => {
const root = ReactDOM.createRoot(document.createElement('div'));
root.render(<App />);
Scheduler.unstable_flushAll();
}).toWarnDev([
'An update to App ran an effect, but was not wrapped in act(...)',
'An update to App ran an effect, but was not wrapped in act(...)',
]);
});
}
it.experimental('warns in concurrent mode', () => {
expect(() => {
const root = ReactDOM.createRoot(document.createElement('div'));
root.render(<App />);
Scheduler.unstable_flushAll();
}).toWarnDev([
'An update to App ran an effect, but was not wrapped in act(...)',
'An update to App ran an effect, but was not wrapped in act(...)',
]);
});
});
});

View File

@ -27,36 +27,30 @@ it('does not warn when rendering in legacy mode', () => {
}).toWarnDev([]);
});
if (__EXPERIMENTAL__) {
it('should warn when rendering in concurrent mode', () => {
expect(() => {
ReactDOM.createRoot(document.createElement('div')).render(<App />);
}).toWarnDev(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
'to guarantee consistent behaviour across tests and browsers.',
{withoutStack: true},
);
// does not warn twice
expect(() => {
ReactDOM.createRoot(document.createElement('div')).render(<App />);
}).toWarnDev([]);
});
it.experimental('should warn when rendering in concurrent mode', () => {
expect(() => {
ReactDOM.createRoot(document.createElement('div')).render(<App />);
}).toWarnDev(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
'to guarantee consistent behaviour across tests and browsers.',
{withoutStack: true},
);
// does not warn twice
expect(() => {
ReactDOM.createRoot(document.createElement('div')).render(<App />);
}).toWarnDev([]);
});
it('should warn when rendering in blocking mode', () => {
expect(() => {
ReactDOM.createBlockingRoot(document.createElement('div')).render(
<App />,
);
}).toWarnDev(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
'to guarantee consistent behaviour across tests and browsers.',
{withoutStack: true},
);
// does not warn twice
expect(() => {
ReactDOM.createBlockingRoot(document.createElement('div')).render(
<App />,
);
}).toWarnDev([]);
});
}
it.experimental('should warn when rendering in blocking mode', () => {
expect(() => {
ReactDOM.createBlockingRoot(document.createElement('div')).render(<App />);
}).toWarnDev(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
'to guarantee consistent behaviour across tests and browsers.',
{withoutStack: true},
);
// does not warn twice
expect(() => {
ReactDOM.createBlockingRoot(document.createElement('div')).render(<App />);
}).toWarnDev([]);
});

View File

@ -474,281 +474,282 @@ describe('ChangeEventPlugin', () => {
}
});
if (__EXPERIMENTAL__) {
describe('concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
});
describe('concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
});
it('text input', () => {
const root = ReactDOM.createRoot(container);
let input;
it.experimental('text input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
it.experimental('checkbox input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
return (
<input
ref={el => (input = el)}
type="checkbox"
checked={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
ops = [];
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(false);
});
it.experimental('textarea', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<textarea
ref={el => (textarea = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
it.experimental('parent of input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<div onChange={this.onChange}>
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
onChange={() => {
// Does nothing. Parent handler is responsible for updating.
}}
/>
);
}
</div>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
it('checkbox input', () => {
const root = ReactDOM.createRoot(container);
let input;
it.experimental('is async for non-input events', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
let ops = [];
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
return (
<input
ref={el => (input = el)}
type="checkbox"
checked={controlledValue}
onChange={this.onChange}
/>
);
}
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
reset = () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
onClick={this.reset}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(true);
// Trigger a click event
input.dispatchEvent(
new Event('click', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(input.value).toBe('initial');
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');
});
ops = [];
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(false);
});
it('textarea', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<textarea
ref={el => (textarea = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
it('parent of input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<div onChange={this.onChange}>
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={() => {
// Does nothing. Parent handler is responsible for updating.
}}
/>
</div>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
it('is async for non-input events', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
reset = () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
onClick={this.reset}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a click event
input.dispatchEvent(
new Event('click', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(input.value).toBe('initial');
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');
});
it('mouse enter/leave should be user-blocking but not discrete', async () => {
it.experimental(
'mouse enter/leave should be user-blocking but not discrete',
async () => {
// This is currently behind a feature flag
jest.resetModules();
React = require('react');
@ -788,7 +789,7 @@ describe('ChangeEventPlugin', () => {
Scheduler.unstable_advanceTime(3000);
expect(container.textContent).toEqual('hovered');
});
});
});
}
},
);
});
});

View File

@ -845,56 +845,54 @@ describe('DOMEventResponderSystem', () => {
buttonRef.current.dispatchEvent(createEvent('foobar'));
});
if (__EXPERIMENTAL__) {
it('should work with concurrent mode updates', async () => {
const log = [];
const TestResponder = createEventResponder({
targetEventTypes: ['click'],
onEvent(event, context, props) {
log.push(props);
},
});
const ref = React.createRef();
function Test({counter}) {
const listener = React.unstable_useResponder(TestResponder, {counter});
Scheduler.unstable_yieldValue('Test');
return (
<button listeners={listener} ref={ref}>
Press me
</button>
);
}
let root = ReactDOM.createRoot(container);
root.render(<Test counter={0} />);
expect(Scheduler).toFlushAndYield(['Test']);
// Click the button
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Increase counter
root.render(<Test counter={1} />);
// Yield before committing
expect(Scheduler).toFlushAndYieldThrough(['Test']);
// Click the button again
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Commit
expect(Scheduler).toFlushAndYield([]);
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 1}]);
it.experimental('should work with concurrent mode updates', async () => {
const log = [];
const TestResponder = createEventResponder({
targetEventTypes: ['click'],
onEvent(event, context, props) {
log.push(props);
},
});
}
const ref = React.createRef();
function Test({counter}) {
const listener = React.unstable_useResponder(TestResponder, {counter});
Scheduler.unstable_yieldValue('Test');
return (
<button listeners={listener} ref={ref}>
Press me
</button>
);
}
let root = ReactDOM.createRoot(container);
root.render(<Test counter={0} />);
expect(Scheduler).toFlushAndYield(['Test']);
// Click the button
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Increase counter
root.render(<Test counter={1} />);
// Yield before committing
expect(Scheduler).toFlushAndYieldThrough(['Test']);
// Click the button again
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Commit
expect(Scheduler).toFlushAndYield([]);
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 1}]);
});
it('should correctly pass through event properties', () => {
const timeStamps = [];

View File

@ -230,17 +230,18 @@ describe('SimpleEventPlugin', function() {
expect(button.textContent).toEqual('Count: 3');
});
if (__EXPERIMENTAL__) {
describe('interactive events, in concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
describe('interactive events, in concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it('flushes pending interactive work before extracting event handler', () => {
it.experimental(
'flushes pending interactive work before extracting event handler',
() => {
container = document.createElement('div');
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
@ -318,9 +319,12 @@ describe('SimpleEventPlugin', function() {
click();
Scheduler.unstable_flushAll();
expect(ops).toEqual([]);
});
},
);
it('end result of many interactive updates is deterministic', () => {
it.experimental(
'end result of many interactive updates is deterministic',
() => {
container = document.createElement('div');
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
@ -373,113 +377,105 @@ describe('SimpleEventPlugin', function() {
Scheduler.unstable_flushAll();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
},
);
it('flushes discrete updates in order', () => {
container = document.createElement('div');
document.body.appendChild(container);
it.experimental('flushes discrete updates in order', () => {
container = document.createElement('div');
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {lowPriCount: 0};
render() {
const text = `High-pri count: ${
this.props.highPriCount
}, Low-pri count: ${this.state.lowPriCount}`;
Scheduler.unstable_yieldValue(text);
return (
<button
ref={el => (button = el)}
onClick={() => {
Scheduler.unstable_next(() => {
this.setState(state => ({
lowPriCount: state.lowPriCount + 1,
}));
});
}}>
{text}
</button>
);
}
}
class Wrapper extends React.Component {
state = {highPriCount: 0};
render() {
return (
<div
onClick={
// Intentionally not using the updater form here, to test
// that updates are serially processed.
() => {
this.setState({highPriCount: this.state.highPriCount + 1});
}
}>
<Button highPriCount={this.state.highPriCount} />
</div>
);
}
}
// Initial mount
const root = ReactDOM.createRoot(container);
root.render(<Wrapper />);
expect(Scheduler).toFlushAndYield([
'High-pri count: 0, Low-pri count: 0',
]);
expect(button.textContent).toEqual(
'High-pri count: 0, Low-pri count: 0',
);
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
let button;
class Button extends React.Component {
state = {lowPriCount: 0};
render() {
const text = `High-pri count: ${
this.props.highPriCount
}, Low-pri count: ${this.state.lowPriCount}`;
Scheduler.unstable_yieldValue(text);
return (
<button
ref={el => (button = el)}
onClick={() => {
Scheduler.unstable_next(() => {
this.setState(state => ({
lowPriCount: state.lowPriCount + 1,
}));
});
}}>
{text}
</button>
);
}
}
// Click the button a single time
click();
// Nothing should flush on the first click.
expect(Scheduler).toHaveYielded([]);
// Click again. This will force the previous discrete update to flush. But
// only the high-pri count will increase.
click();
expect(Scheduler).toHaveYielded([
'High-pri count: 1, Low-pri count: 0',
]);
expect(button.textContent).toEqual(
'High-pri count: 1, Low-pri count: 0',
class Wrapper extends React.Component {
state = {highPriCount: 0};
render() {
return (
<div
onClick={
// Intentionally not using the updater form here, to test
// that updates are serially processed.
() => {
this.setState({highPriCount: this.state.highPriCount + 1});
}
}>
<Button highPriCount={this.state.highPriCount} />
</div>
);
}
}
// Initial mount
const root = ReactDOM.createRoot(container);
root.render(<Wrapper />);
expect(Scheduler).toFlushAndYield([
'High-pri count: 0, Low-pri count: 0',
]);
expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Click the button a single time
click();
// Nothing should flush on the first click.
expect(Scheduler).toHaveYielded([]);
// Click again. This will force the previous discrete update to flush. But
// only the high-pri count will increase.
click();
expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
// Flush the remaining work.
expect(Scheduler).toHaveYielded([
'High-pri count: 2, Low-pri count: 0',
'High-pri count: 3, Low-pri count: 0',
'High-pri count: 4, Low-pri count: 0',
'High-pri count: 5, Low-pri count: 0',
'High-pri count: 6, Low-pri count: 0',
'High-pri count: 7, Low-pri count: 0',
]);
// Click the button many more times
click();
click();
click();
click();
click();
click();
// At the end, both counters should equal the total number of clicks
expect(Scheduler).toFlushAndYield([
'High-pri count: 8, Low-pri count: 0',
'High-pri count: 8, Low-pri count: 8',
]);
expect(button.textContent).toEqual(
'High-pri count: 8, Low-pri count: 8',
);
});
// Flush the remaining work.
expect(Scheduler).toHaveYielded([
'High-pri count: 2, Low-pri count: 0',
'High-pri count: 3, Low-pri count: 0',
'High-pri count: 4, Low-pri count: 0',
'High-pri count: 5, Low-pri count: 0',
'High-pri count: 6, Low-pri count: 0',
'High-pri count: 7, Low-pri count: 0',
]);
// At the end, both counters should equal the total number of clicks
expect(Scheduler).toFlushAndYield([
'High-pri count: 8, Low-pri count: 0',
'High-pri count: 8, Low-pri count: 8',
]);
expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
});
}
});
describe('iOS bubbling click fix', function() {
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html

View File

@ -750,208 +750,202 @@ describe('Input event responder', () => {
}
});
if (__EXPERIMENTAL__) {
describe('concurrent mode', () => {
it('text input', () => {
const root = ReactDOM.createRoot(container);
let input;
describe('concurrent mode', () => {
it.experimental('text input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
return (
<input
type="text"
ref={innerRef}
value={controlledValue}
listeners={listener}
/>
);
}
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
type="text"
ref={innerRef}
value={controlledValue}
listeners={listener}
<Component
onChange={this.onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
/>
);
}
}
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed'
? 'changed [!]'
: this.state.value;
return (
<Component
onChange={this.onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
ops = [];
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
it('checkbox input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
return (
<input
type="checkbox"
ref={innerRef}
checked={controlledValue}
listeners={listener}
/>
);
}
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
return (
<Component
controlledValue={controlledValue}
onChange={this.onChange}
innerRef={el => (input = el)}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
ops = [];
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(false);
});
it('textarea', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
return (
<textarea
type="text"
ref={innerRef}
value={controlledValue}
listeners={listener}
/>
);
}
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed'
? 'changed [!]'
: this.state.value;
return (
<Component
onChange={this.onChange}
innerRef={el => (textarea = el)}
controlledValue={controlledValue}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
}
it.experimental('checkbox input', () => {
const root = ReactDOM.createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
return (
<input
type="checkbox"
ref={innerRef}
checked={controlledValue}
listeners={listener}
/>
);
}
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
ops.push(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
return (
<Component
controlledValue={controlledValue}
onChange={this.onChange}
innerRef={el => (input = el)}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
ops = [];
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
Scheduler.unstable_flushAll();
ops = [];
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: true']);
expect(input.checked).toBe(false);
});
it.experimental('textarea', () => {
const root = ReactDOM.createRoot(container);
let textarea;
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInput({
onChange,
});
return (
<textarea
type="text"
ref={innerRef}
value={controlledValue}
listeners={listener}
/>
);
}
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<Component
onChange={this.onChange}
innerRef={el => (textarea = el)}
controlledValue={controlledValue}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
ops = [];
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(ops).toEqual(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
});
});
it('expect displayName to show up for event component', () => {

View File

@ -1973,9 +1973,10 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(totalRefUpdates).toBe(2); // Should not increase since last time
});
});
if (__EXPERIMENTAL__) {
describe('useTransition', () => {
it('delays showing loading state until after timeout', async () => {
describe('useTransition', () => {
it.experimental(
'delays showing loading state until after timeout',
async () => {
let transition;
function App() {
const [show, setShow] = useState(false);
@ -2037,8 +2038,11 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([
span('After... Pending: false'),
]);
});
it('delays showing loading state until after busyDelayMs + busyMinDurationMs', async () => {
},
);
it.experimental(
'delays showing loading state until after busyDelayMs + busyMinDurationMs',
async () => {
let transition;
function App() {
const [show, setShow] = useState(false);
@ -2105,81 +2109,82 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([
span('After... Pending: false'),
]);
},
);
});
describe('useDeferredValue', () => {
it.experimental('defers text value until specified timeout', async () => {
function TextBox({text}) {
return <AsyncText ms={1000} text={text} />;
}
let _setText;
function App() {
const [text, setText] = useState('A');
const deferredText = useDeferredValue(text, {
timeoutMs: 500,
});
_setText = setText;
return (
<>
<Text text={text} />
<Suspense fallback={<Text text={'Loading'} />}>
<TextBox text={deferredText} />
</Suspense>
</>
);
}
act(() => {
ReactNoop.render(<App />);
});
});
describe('useDeferredValue', () => {
it('defers text value until specified timeout', async () => {
function TextBox({text}) {
return <AsyncText ms={1000} text={text} />;
}
let _setText;
function App() {
const [text, setText] = useState('A');
const deferredText = useDeferredValue(text, {
timeoutMs: 500,
});
_setText = setText;
return (
<>
<Text text={text} />
<Suspense fallback={<Text text={'Loading'} />}>
<TextBox text={deferredText} />
</Suspense>
</>
);
}
expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]);
act(() => {
ReactNoop.render(<App />);
});
Scheduler.unstable_advanceTime(1000);
await advanceTimers(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]);
Scheduler.unstable_advanceTime(1000);
await advanceTimers(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
act(() => {
_setText('B');
});
expect(Scheduler).toHaveYielded([
'B',
'A',
'B',
'Suspend! [B]',
'Loading',
]);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
Scheduler.unstable_advanceTime(250);
await advanceTimers(250);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
Scheduler.unstable_advanceTime(500);
await advanceTimers(500);
expect(ReactNoop.getChildren()).toEqual([
span('B'),
hiddenSpan('A'),
span('Loading'),
]);
Scheduler.unstable_advanceTime(250);
await advanceTimers(250);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
act(() => {
expect(Scheduler).toFlushAndYield(['B']);
});
expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
act(() => {
_setText('B');
});
expect(Scheduler).toHaveYielded([
'B',
'A',
'B',
'Suspend! [B]',
'Loading',
]);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
Scheduler.unstable_advanceTime(250);
await advanceTimers(250);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
Scheduler.unstable_advanceTime(500);
await advanceTimers(500);
expect(ReactNoop.getChildren()).toEqual([
span('B'),
hiddenSpan('A'),
span('Loading'),
]);
Scheduler.unstable_advanceTime(250);
await advanceTimers(250);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
act(() => {
expect(Scheduler).toFlushAndYield(['B']);
});
expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
});
}
});
describe('progressive enhancement (not supported)', () => {
it('mount additional state', () => {
let updateA;