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:
parent
a1ff9fd7bb
commit
6dc2734b41
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(...)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue