mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Codemod it.experimental to gate pragma (#18582)
* Codemod it.experimental to gate pragma Find-and-replace followed by Prettier * Delete it.experimental Removes the API from our test setup script
This commit is contained in:
+235
-241
@@ -153,7 +153,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it.experimental('does not perform deferred updates synchronously', () => {
|
||||
// @gate experimental
|
||||
it('does not perform deferred updates synchronously', () => {
|
||||
const inputRef = React.createRef();
|
||||
const asyncValueRef = React.createRef();
|
||||
const syncValueRef = React.createRef();
|
||||
@@ -208,7 +209,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(syncValueRef.current.textContent).toBe('hello');
|
||||
});
|
||||
|
||||
it.experimental('top-level updates are concurrent', () => {
|
||||
// @gate experimental
|
||||
it('top-level updates are concurrent', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<div>Hi</div>);
|
||||
expect(container.textContent).toEqual('');
|
||||
@@ -221,7 +223,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(container.textContent).toEqual('Bye');
|
||||
});
|
||||
|
||||
it.experimental('deep updates (setState) are concurrent', () => {
|
||||
// @gate experimental
|
||||
it('deep updates (setState) are concurrent', () => {
|
||||
let instance;
|
||||
class Component extends React.Component {
|
||||
state = {step: 0};
|
||||
@@ -243,7 +246,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(container.textContent).toEqual('1');
|
||||
});
|
||||
|
||||
it.experimental('flushSync flushes updates before end of the tick', () => {
|
||||
// @gate experimental
|
||||
it('flushSync flushes updates before end of the tick', () => {
|
||||
const ops = [];
|
||||
let instance;
|
||||
|
||||
@@ -291,82 +295,79 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(ops).toEqual(['BC', 'ABCD']);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'flushControlled flushes updates before yielding to browser',
|
||||
() => {
|
||||
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;
|
||||
}
|
||||
// @gate experimental
|
||||
it('flushControlled flushes updates before yielding to browser', () => {
|
||||
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;
|
||||
}
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Counter />);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('0');
|
||||
}
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Counter />);
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('0');
|
||||
|
||||
// Test that a normal update is async
|
||||
// Test that a normal update is async
|
||||
inst.increment();
|
||||
expect(container.textContent).toEqual('0');
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('1');
|
||||
|
||||
const ops = [];
|
||||
ReactDOM.unstable_flushControlled(() => {
|
||||
inst.increment();
|
||||
expect(container.textContent).toEqual('0');
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('1');
|
||||
|
||||
const ops = [];
|
||||
ReactDOM.unstable_flushControlled(() => {
|
||||
inst.increment();
|
||||
ReactDOM.unstable_flushControlled(() => {
|
||||
inst.increment();
|
||||
ops.push('end of inner flush: ' + container.textContent);
|
||||
});
|
||||
ops.push('end of outer flush: ' + container.textContent);
|
||||
ops.push('end of inner flush: ' + container.textContent);
|
||||
});
|
||||
ops.push('after outer flush: ' + container.textContent);
|
||||
expect(ops).toEqual([
|
||||
'end of inner flush: 1',
|
||||
'end of outer flush: 1',
|
||||
'after outer flush: 3',
|
||||
]);
|
||||
},
|
||||
);
|
||||
ops.push('end of outer flush: ' + container.textContent);
|
||||
});
|
||||
ops.push('after outer flush: ' + container.textContent);
|
||||
expect(ops).toEqual([
|
||||
'end of inner flush: 1',
|
||||
'end of outer flush: 1',
|
||||
'after outer flush: 3',
|
||||
]);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'flushControlled does not flush until end of outermost batchedUpdates',
|
||||
() => {
|
||||
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;
|
||||
}
|
||||
// @gate experimental
|
||||
it('flushControlled does not flush until end of outermost batchedUpdates', () => {
|
||||
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);
|
||||
}
|
||||
ReactDOM.render(<Counter />, container);
|
||||
|
||||
const ops = [];
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
const ops = [];
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
inst.increment();
|
||||
ReactDOM.unstable_flushControlled(() => {
|
||||
inst.increment();
|
||||
ReactDOM.unstable_flushControlled(() => {
|
||||
inst.increment();
|
||||
ops.push('end of flushControlled fn: ' + container.textContent);
|
||||
});
|
||||
ops.push('end of batchedUpdates fn: ' + container.textContent);
|
||||
ops.push('end of flushControlled fn: ' + container.textContent);
|
||||
});
|
||||
ops.push('after batchedUpdates: ' + container.textContent);
|
||||
expect(ops).toEqual([
|
||||
'end of flushControlled fn: 0',
|
||||
'end of batchedUpdates fn: 0',
|
||||
'after batchedUpdates: 2',
|
||||
]);
|
||||
},
|
||||
);
|
||||
ops.push('end of batchedUpdates fn: ' + container.textContent);
|
||||
});
|
||||
ops.push('after batchedUpdates: ' + container.textContent);
|
||||
expect(ops).toEqual([
|
||||
'end of flushControlled fn: 0',
|
||||
'end of batchedUpdates fn: 0',
|
||||
'after batchedUpdates: 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it.experimental('flushControlled returns nothing', () => {
|
||||
// @gate experimental
|
||||
it('flushControlled returns nothing', () => {
|
||||
// In the future, we may want to return a thenable "work" object.
|
||||
let inst;
|
||||
class Counter extends React.Component {
|
||||
@@ -389,201 +390,193 @@ describe('ReactDOMFiberAsync', () => {
|
||||
expect(returnValue).toBe(undefined);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'ignores discrete events on a pending removed element',
|
||||
() => {
|
||||
const disableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
// @gate experimental
|
||||
it('ignores discrete events on a pending removed element', () => {
|
||||
const disableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
|
||||
let formSubmitted = false;
|
||||
let formSubmitted = false;
|
||||
|
||||
class Form extends React.Component {
|
||||
state = {active: true};
|
||||
disableForm = () => {
|
||||
this.setState({active: false});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should not get invoked
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.disableForm} ref={disableButtonRef}>
|
||||
Disable
|
||||
</button>
|
||||
{this.state.active ? (
|
||||
<button onClick={this.submitForm} ref={submitButtonRef}>
|
||||
Submit
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const disableButton = disableButtonRef.current;
|
||||
expect(disableButton.tagName).toBe('BUTTON');
|
||||
|
||||
// Dispatch a click event on the Disable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
disableButton.dispatchEvent(firstEvent);
|
||||
|
||||
// There should now be a pending update to disable the form.
|
||||
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which disables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
|
||||
// Therefore the form should never have been submitted.
|
||||
expect(formSubmitted).toBe(false);
|
||||
|
||||
expect(submitButtonRef.current).toBe(null);
|
||||
},
|
||||
);
|
||||
|
||||
it.experimental(
|
||||
'ignores discrete events on a pending removed event listener',
|
||||
() => {
|
||||
const disableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
|
||||
let formSubmitted = false;
|
||||
|
||||
class Form extends React.Component {
|
||||
state = {active: true};
|
||||
disableForm = () => {
|
||||
this.setState({active: false});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should not get invoked
|
||||
};
|
||||
disabledSubmitForm = () => {
|
||||
// The form is disabled.
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.disableForm} ref={disableButtonRef}>
|
||||
Disable
|
||||
</button>
|
||||
<button
|
||||
onClick={
|
||||
this.state.active
|
||||
? this.submitForm
|
||||
: this.disabledSubmitForm
|
||||
}
|
||||
ref={submitButtonRef}>
|
||||
class Form extends React.Component {
|
||||
state = {active: true};
|
||||
disableForm = () => {
|
||||
this.setState({active: false});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should not get invoked
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.disableForm} ref={disableButtonRef}>
|
||||
Disable
|
||||
</button>
|
||||
{this.state.active ? (
|
||||
<button onClick={this.submitForm} ref={submitButtonRef}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const disableButton = disableButtonRef.current;
|
||||
expect(disableButton.tagName).toBe('BUTTON');
|
||||
const disableButton = disableButtonRef.current;
|
||||
expect(disableButton.tagName).toBe('BUTTON');
|
||||
|
||||
// Dispatch a click event on the Disable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
disableButton.dispatchEvent(firstEvent);
|
||||
// Dispatch a click event on the Disable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
disableButton.dispatchEvent(firstEvent);
|
||||
|
||||
// There should now be a pending update to disable the form.
|
||||
// There should now be a pending update to disable the form.
|
||||
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which disables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which disables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
|
||||
// Therefore the form should never have been submitted.
|
||||
expect(formSubmitted).toBe(false);
|
||||
},
|
||||
);
|
||||
// Therefore the form should never have been submitted.
|
||||
expect(formSubmitted).toBe(false);
|
||||
|
||||
it.experimental(
|
||||
'uses the newest discrete events on a pending changed event listener',
|
||||
() => {
|
||||
const enableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
expect(submitButtonRef.current).toBe(null);
|
||||
});
|
||||
|
||||
let formSubmitted = false;
|
||||
// @gate experimental
|
||||
it('ignores discrete events on a pending removed event listener', () => {
|
||||
const disableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
|
||||
class Form extends React.Component {
|
||||
state = {active: false};
|
||||
enableForm = () => {
|
||||
this.setState({active: true});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should happen
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.enableForm} ref={enableButtonRef}>
|
||||
Enable
|
||||
</button>
|
||||
<button
|
||||
onClick={this.state.active ? this.submitForm : null}
|
||||
ref={submitButtonRef}>
|
||||
Submit
|
||||
</button>{' '}
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let formSubmitted = false;
|
||||
|
||||
class Form extends React.Component {
|
||||
state = {active: true};
|
||||
disableForm = () => {
|
||||
this.setState({active: false});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should not get invoked
|
||||
};
|
||||
disabledSubmitForm = () => {
|
||||
// The form is disabled.
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.disableForm} ref={disableButtonRef}>
|
||||
Disable
|
||||
</button>
|
||||
<button
|
||||
onClick={
|
||||
this.state.active ? this.submitForm : this.disabledSubmitForm
|
||||
}
|
||||
ref={submitButtonRef}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const enableButton = enableButtonRef.current;
|
||||
expect(enableButton.tagName).toBe('BUTTON');
|
||||
const disableButton = disableButtonRef.current;
|
||||
expect(disableButton.tagName).toBe('BUTTON');
|
||||
|
||||
// Dispatch a click event on the Enable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
enableButton.dispatchEvent(firstEvent);
|
||||
// Dispatch a click event on the Disable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
disableButton.dispatchEvent(firstEvent);
|
||||
|
||||
// There should now be a pending update to enable the form.
|
||||
// There should now be a pending update to disable the form.
|
||||
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which enables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which disables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
|
||||
// Therefore the form should have been submitted.
|
||||
expect(formSubmitted).toBe(true);
|
||||
},
|
||||
);
|
||||
// Therefore the form should never have been submitted.
|
||||
expect(formSubmitted).toBe(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('uses the newest discrete events on a pending changed event listener', () => {
|
||||
const enableButtonRef = React.createRef();
|
||||
const submitButtonRef = React.createRef();
|
||||
|
||||
let formSubmitted = false;
|
||||
|
||||
class Form extends React.Component {
|
||||
state = {active: false};
|
||||
enableForm = () => {
|
||||
this.setState({active: true});
|
||||
};
|
||||
submitForm = () => {
|
||||
formSubmitted = true; // This should happen
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.enableForm} ref={enableButtonRef}>
|
||||
Enable
|
||||
</button>
|
||||
<button
|
||||
onClick={this.state.active ? this.submitForm : null}
|
||||
ref={submitButtonRef}>
|
||||
Submit
|
||||
</button>{' '}
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Form />);
|
||||
// Flush
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const enableButton = enableButtonRef.current;
|
||||
expect(enableButton.tagName).toBe('BUTTON');
|
||||
|
||||
// Dispatch a click event on the Enable-button.
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
enableButton.dispatchEvent(firstEvent);
|
||||
|
||||
// There should now be a pending update to enable the form.
|
||||
|
||||
// This should not have flushed yet since it's in concurrent mode.
|
||||
const submitButton = submitButtonRef.current;
|
||||
expect(submitButton.tagName).toBe('BUTTON');
|
||||
|
||||
// In the meantime, we can dispatch a new client event on the submit button.
|
||||
const secondEvent = document.createEvent('Event');
|
||||
secondEvent.initEvent('click', true, true);
|
||||
// This should force the pending update to flush which enables the submit button before the event is invoked.
|
||||
submitButton.dispatchEvent(secondEvent);
|
||||
|
||||
// Therefore the form should have been submitted.
|
||||
expect(formSubmitted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('regression test: does not drop passive effects across roots (#17066)', () => {
|
||||
@@ -617,7 +610,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
});
|
||||
|
||||
describe('createBlockingRoot', () => {
|
||||
it.experimental('updates flush without yielding in the next event', () => {
|
||||
// @gate experimental
|
||||
it('updates flush without yielding in the next event', () => {
|
||||
const root = ReactDOM.createBlockingRoot(container);
|
||||
|
||||
function Text(props) {
|
||||
|
||||
+30
-32
@@ -105,41 +105,39 @@ describe('ReactDOMHooks', () => {
|
||||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'should not bail out when an update is scheduled from within an event handler in Concurrent Mode',
|
||||
() => {
|
||||
const {createRef, useCallback, useState} = React;
|
||||
// @gate experimental
|
||||
it('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}) => {
|
||||
const [text, setText] = useState('');
|
||||
const handleInput = useCallback(event => {
|
||||
setText(event.target.value);
|
||||
});
|
||||
const Example = ({inputRef, labelRef}) => {
|
||||
const [text, setText] = useState('');
|
||||
const handleInput = useCallback(event => {
|
||||
setText(event.target.value);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={inputRef} onInput={handleInput} />
|
||||
<label ref={labelRef}>{text}</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const inputRef = createRef();
|
||||
const labelRef = createRef();
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
inputRef.current.value = 'abc';
|
||||
inputRef.current.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
return (
|
||||
<>
|
||||
<input ref={inputRef} onInput={handleInput} />
|
||||
<label ref={labelRef}>{text}</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
const inputRef = createRef();
|
||||
const labelRef = createRef();
|
||||
|
||||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
},
|
||||
);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
inputRef.current.value = 'abc';
|
||||
inputRef.current.dispatchEvent(
|
||||
new Event('input', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(labelRef.current.innerHTML).toBe('abc');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,23 +491,21 @@ describe('ReactDOMServerHydration', () => {
|
||||
expect(element.textContent).toBe('Hello world');
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'does not re-enter hydration after committing the first one',
|
||||
() => {
|
||||
const finalHTML = ReactDOMServer.renderToString(<div />);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(null);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This should not reenter hydration state and therefore not trigger hydration
|
||||
// warnings.
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
},
|
||||
);
|
||||
// @gate experimental
|
||||
it('does not re-enter hydration after committing the first one', () => {
|
||||
const finalHTML = ReactDOMServer.renderToString(<div />);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = finalHTML;
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(null);
|
||||
Scheduler.unstable_flushAll();
|
||||
// This should not reenter hydration state and therefore not trigger hydration
|
||||
// warnings.
|
||||
root.render(<div />);
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
it('Suspense + hydration in legacy mode', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
@@ -124,7 +124,8 @@ describe('ReactTestUtils.act()', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.experimental('warns in blocking mode', () => {
|
||||
// @gate experimental
|
||||
it('warns in blocking mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.createBlockingRoot(document.createElement('div'));
|
||||
root.render(<App />);
|
||||
@@ -134,7 +135,8 @@ describe('ReactTestUtils.act()', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.experimental('warns in concurrent mode', () => {
|
||||
// @gate experimental
|
||||
it('warns in concurrent mode', () => {
|
||||
expect(() => {
|
||||
const root = ReactDOM.createRoot(document.createElement('div'));
|
||||
root.render(<App />);
|
||||
|
||||
@@ -27,7 +27,8 @@ it('does not warn when rendering in legacy mode', () => {
|
||||
}).toErrorDev([]);
|
||||
});
|
||||
|
||||
it.experimental('should warn when rendering in concurrent mode', () => {
|
||||
// @gate experimental
|
||||
it('should warn when rendering in concurrent mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.createRoot(document.createElement('div')).render(<App />);
|
||||
}).toErrorDev(
|
||||
@@ -41,7 +42,8 @@ it.experimental('should warn when rendering in concurrent mode', () => {
|
||||
}).toErrorDev([]);
|
||||
});
|
||||
|
||||
it.experimental('should warn when rendering in blocking mode', () => {
|
||||
// @gate experimental
|
||||
it('should warn when rendering in blocking mode', () => {
|
||||
expect(() => {
|
||||
ReactDOM.createBlockingRoot(document.createElement('div')).render(<App />);
|
||||
}).toErrorDev(
|
||||
|
||||
+49
-51
@@ -1287,64 +1287,62 @@ describe('ReactUpdates', () => {
|
||||
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'delays sync updates inside hidden subtrees in Concurrent Mode',
|
||||
() => {
|
||||
const container = document.createElement('div');
|
||||
// @gate experimental
|
||||
it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
function Baz() {
|
||||
Scheduler.unstable_yieldValue('Baz');
|
||||
return <p>baz</p>;
|
||||
}
|
||||
function Baz() {
|
||||
Scheduler.unstable_yieldValue('Baz');
|
||||
return <p>baz</p>;
|
||||
}
|
||||
|
||||
let setCounter;
|
||||
function Bar() {
|
||||
const [counter, _setCounter] = React.useState(0);
|
||||
setCounter = _setCounter;
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
return <p>bar {counter}</p>;
|
||||
}
|
||||
let setCounter;
|
||||
function Bar() {
|
||||
const [counter, _setCounter] = React.useState(0);
|
||||
setCounter = _setCounter;
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
return <p>bar {counter}</p>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
React.useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Foo#effect');
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div hidden={true}>
|
||||
<Bar />
|
||||
</div>
|
||||
<Baz />
|
||||
function Foo() {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
React.useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Foo#effect');
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div hidden={true}>
|
||||
<Bar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let hiddenDiv;
|
||||
act(() => {
|
||||
root.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
|
||||
hiddenDiv = container.firstChild.firstChild;
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('');
|
||||
// Run offscreen update
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
});
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
setCounter(1);
|
||||
});
|
||||
// Should not flush yet
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
<Baz />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let hiddenDiv;
|
||||
act(() => {
|
||||
root.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
|
||||
hiddenDiv = container.firstChild.firstChild;
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('');
|
||||
// Run offscreen update
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
|
||||
},
|
||||
);
|
||||
expect(hiddenDiv.hidden).toBe(true);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
});
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
setCounter(1);
|
||||
});
|
||||
// Should not flush yet
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
|
||||
|
||||
// Run offscreen update
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
|
||||
});
|
||||
|
||||
it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
|
||||
class Foo extends React.Component {
|
||||
|
||||
+166
-174
@@ -432,87 +432,85 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(log).toEqual(['second', 'first']);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'does not invoke an event on a parent tree when a subtree is dehydrated',
|
||||
async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(
|
||||
resolvePromise => (resolve = resolvePromise),
|
||||
// @gate experimental
|
||||
it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(
|
||||
resolvePromise => (resolve = resolvePromise),
|
||||
);
|
||||
|
||||
let clicks = 0;
|
||||
const childSlotRef = React.createRef();
|
||||
|
||||
function Parent() {
|
||||
return <div onClick={() => clicks++} ref={childSlotRef} />;
|
||||
}
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <a>Click me</a>;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
// The root is a Suspense boundary.
|
||||
return (
|
||||
<React.Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
let clicks = 0;
|
||||
const childSlotRef = React.createRef();
|
||||
suspend = false;
|
||||
const finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
function Parent() {
|
||||
return <div onClick={() => clicks++} ref={childSlotRef} />;
|
||||
}
|
||||
const parentContainer = document.createElement('div');
|
||||
const childContainer = document.createElement('div');
|
||||
|
||||
function Child({text}) {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <a>Click me</a>;
|
||||
}
|
||||
}
|
||||
// We need this to be in the document since we'll dispatch events on it.
|
||||
document.body.appendChild(parentContainer);
|
||||
|
||||
function App() {
|
||||
// The root is a Suspense boundary.
|
||||
return (
|
||||
<React.Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
// We're going to use a different root as a parent.
|
||||
// This lets us detect whether an event goes through React's event system.
|
||||
const parentRoot = ReactDOM.createRoot(parentContainer);
|
||||
parentRoot.render(<Parent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
suspend = false;
|
||||
const finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
childSlotRef.current.appendChild(childContainer);
|
||||
|
||||
const parentContainer = document.createElement('div');
|
||||
const childContainer = document.createElement('div');
|
||||
childContainer.innerHTML = finalHTML;
|
||||
|
||||
// We need this to be in the document since we'll dispatch events on it.
|
||||
document.body.appendChild(parentContainer);
|
||||
const a = childContainer.getElementsByTagName('a')[0];
|
||||
|
||||
// We're going to use a different root as a parent.
|
||||
// This lets us detect whether an event goes through React's event system.
|
||||
const parentRoot = ReactDOM.createRoot(parentContainer);
|
||||
parentRoot.render(<Parent />);
|
||||
Scheduler.unstable_flushAll();
|
||||
suspend = true;
|
||||
|
||||
childSlotRef.current.appendChild(childContainer);
|
||||
// Hydrate asynchronously.
|
||||
const root = ReactDOM.createRoot(childContainer, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
childContainer.innerHTML = finalHTML;
|
||||
// The Suspense boundary is not yet hydrated.
|
||||
a.click();
|
||||
expect(clicks).toBe(0);
|
||||
|
||||
const a = childContainer.getElementsByTagName('a')[0];
|
||||
// Resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
suspend = true;
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
||||
// Hydrate asynchronously.
|
||||
const root = ReactDOM.createRoot(childContainer, {hydrate: true});
|
||||
root.render(<App />);
|
||||
jest.runAllTimers();
|
||||
Scheduler.unstable_flushAll();
|
||||
// We're now full hydrated.
|
||||
|
||||
// The Suspense boundary is not yet hydrated.
|
||||
a.click();
|
||||
expect(clicks).toBe(0);
|
||||
expect(clicks).toBe(1);
|
||||
|
||||
// Resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
||||
// We're now full hydrated.
|
||||
|
||||
expect(clicks).toBe(1);
|
||||
|
||||
document.body.removeChild(parentContainer);
|
||||
},
|
||||
);
|
||||
document.body.removeChild(parentContainer);
|
||||
});
|
||||
|
||||
it('handle click events on dynamic portals', () => {
|
||||
const log = [];
|
||||
@@ -1679,55 +1677,53 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(targetListerner4).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'should work with concurrent mode updates',
|
||||
async () => {
|
||||
const log = [];
|
||||
const ref = React.createRef();
|
||||
// @gate experimental
|
||||
it('should work with concurrent mode updates', async () => {
|
||||
const log = [];
|
||||
const ref = React.createRef();
|
||||
|
||||
function Test({counter}) {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
function Test({counter}) {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
click.setListener(ref.current, () => {
|
||||
log.push({counter});
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
click.setListener(ref.current, () => {
|
||||
log.push({counter});
|
||||
});
|
||||
});
|
||||
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return <button ref={ref}>Press me</button>;
|
||||
}
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return <button ref={ref}>Press me</button>;
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Test counter={0} />);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Test counter={0} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
root.render(<Test counter={1} />);
|
||||
// Yield before committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
// 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}]);
|
||||
// Click the button again
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 0}]);
|
||||
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
// Clear log
|
||||
log.length = 0;
|
||||
|
||||
// Commit
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
},
|
||||
);
|
||||
// Commit
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
});
|
||||
|
||||
it('should correctly work for a basic "click" listener that upgrades', () => {
|
||||
const clickEvent = jest.fn();
|
||||
@@ -2259,86 +2255,82 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(log).toEqual(['beforeblur', 'afterblur']);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'beforeblur and afterblur are called after a focused element is suspended',
|
||||
() => {
|
||||
const log = [];
|
||||
// We have to persist here because we want to read relatedTarget later.
|
||||
const onAfterBlur = jest.fn(e => {
|
||||
e.persist();
|
||||
log.push(e.type);
|
||||
});
|
||||
const onBeforeBlur = jest.fn(e => log.push(e.type));
|
||||
const innerRef = React.createRef();
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(
|
||||
resolvePromise => (resolve = resolvePromise),
|
||||
);
|
||||
// @gate experimental
|
||||
it('beforeblur and afterblur are called after a focused element is suspended', () => {
|
||||
const log = [];
|
||||
// We have to persist here because we want to read relatedTarget later.
|
||||
const onAfterBlur = jest.fn(e => {
|
||||
e.persist();
|
||||
log.push(e.type);
|
||||
});
|
||||
const onBeforeBlur = jest.fn(e => log.push(e.type));
|
||||
const innerRef = React.createRef();
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(
|
||||
resolvePromise => (resolve = resolvePromise),
|
||||
);
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Component = () => {
|
||||
const ref = React.useRef(null);
|
||||
const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
|
||||
const beforeBlurHandle = ReactDOM.unstable_useEvent(
|
||||
'beforeblur',
|
||||
);
|
||||
const Component = () => {
|
||||
const ref = React.useRef(null);
|
||||
const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
|
||||
const beforeBlurHandle = ReactDOM.unstable_useEvent('beforeblur');
|
||||
|
||||
React.useEffect(() => {
|
||||
afterBlurHandle.setListener(document, onAfterBlur);
|
||||
beforeBlurHandle.setListener(ref.current, onBeforeBlur);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const container2 = document.createElement('div');
|
||||
document.body.appendChild(container2);
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
root.render(<Component />);
|
||||
React.useEffect(() => {
|
||||
afterBlurHandle.setListener(document, onAfterBlur);
|
||||
beforeBlurHandle.setListener(ref.current, onBeforeBlur);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.focus();
|
||||
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlur).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
ReactTestUtils.act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlur).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlur).toHaveBeenCalledWith(
|
||||
expect.objectContaining({relatedTarget: inner}),
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
resolve();
|
||||
expect(log).toEqual(['beforeblur', 'afterblur']);
|
||||
};
|
||||
|
||||
document.body.removeChild(container2);
|
||||
},
|
||||
);
|
||||
const container2 = document.createElement('div');
|
||||
document.body.appendChild(container2);
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.focus();
|
||||
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlur).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
ReactTestUtils.act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlur).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlur).toHaveBeenCalledWith(
|
||||
expect.objectContaining({relatedTarget: inner}),
|
||||
);
|
||||
resolve();
|
||||
expect(log).toEqual(['beforeblur', 'afterblur']);
|
||||
|
||||
document.body.removeChild(container2);
|
||||
});
|
||||
|
||||
describe('Compatibility with Scopes API', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
+2
-1
@@ -789,7 +789,8 @@ describe('DOMEventResponderSystem', () => {
|
||||
buttonRef.current.dispatchEvent(createEvent('foobar'));
|
||||
});
|
||||
|
||||
it.experimental('should work with concurrent mode updates', async () => {
|
||||
// @gate experimental
|
||||
it('should work with concurrent mode updates', async () => {
|
||||
const log = [];
|
||||
const TestResponder = createEventResponder({
|
||||
targetEventTypes: ['click'],
|
||||
|
||||
+47
-44
@@ -477,7 +477,8 @@ describe('ChangeEventPlugin', () => {
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it.experimental('text input', () => {
|
||||
// @gate experimental
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -519,7 +520,8 @@ describe('ChangeEventPlugin', () => {
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it.experimental('checkbox input', () => {
|
||||
// @gate experimental
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -574,7 +576,8 @@ describe('ChangeEventPlugin', () => {
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it.experimental('textarea', () => {
|
||||
// @gate experimental
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let textarea;
|
||||
|
||||
@@ -616,7 +619,8 @@ describe('ChangeEventPlugin', () => {
|
||||
expect(textarea.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it.experimental('parent of input', () => {
|
||||
// @gate experimental
|
||||
it('parent of input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -662,7 +666,8 @@ describe('ChangeEventPlugin', () => {
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it.experimental('is async for non-input events', () => {
|
||||
// @gate experimental
|
||||
it('is async for non-input events', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -711,50 +716,48 @@ describe('ChangeEventPlugin', () => {
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
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');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
// @gate experimental
|
||||
it('mouse enter/leave should be user-blocking but not discrete', async () => {
|
||||
// This is currently behind a feature flag
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
const {act} = TestUtils;
|
||||
const {useState} = React;
|
||||
const {act} = TestUtils;
|
||||
const {useState} = React;
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
const target = React.createRef(null);
|
||||
function Foo() {
|
||||
const [isHover, setHover] = useState(false);
|
||||
return (
|
||||
<div
|
||||
ref={target}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}>
|
||||
{isHover ? 'hovered' : 'not hovered'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const target = React.createRef(null);
|
||||
function Foo() {
|
||||
const [isHover, setHover] = useState(false);
|
||||
return (
|
||||
<div
|
||||
ref={target}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}>
|
||||
{isHover ? 'hovered' : 'not hovered'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Foo />);
|
||||
});
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
await act(async () => {
|
||||
root.render(<Foo />);
|
||||
});
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
|
||||
await act(async () => {
|
||||
const mouseOverEvent = document.createEvent('MouseEvents');
|
||||
mouseOverEvent.initEvent('mouseover', true, true);
|
||||
target.current.dispatchEvent(mouseOverEvent);
|
||||
await act(async () => {
|
||||
const mouseOverEvent = document.createEvent('MouseEvents');
|
||||
mouseOverEvent.initEvent('mouseover', true, true);
|
||||
target.current.dispatchEvent(mouseOverEvent);
|
||||
|
||||
// 3s should be enough to expire the updates
|
||||
Scheduler.unstable_advanceTime(3000);
|
||||
expect(Scheduler).toFlushExpired([]);
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
});
|
||||
},
|
||||
);
|
||||
// 3s should be enough to expire the updates
|
||||
Scheduler.unstable_advanceTime(3000);
|
||||
expect(Scheduler).toFlushExpired([]);
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+113
-116
@@ -236,138 +236,135 @@ describe('SimpleEventPlugin', function() {
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'flushes pending interactive work before extracting event handler',
|
||||
() => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
// @gate experimental
|
||||
it('flushes pending interactive work before extracting event handler', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {disabled: false};
|
||||
onClick = () => {
|
||||
// Perform some side-effect
|
||||
Scheduler.unstable_yieldValue('Side-effect');
|
||||
// Disable the button
|
||||
this.setState({disabled: true});
|
||||
};
|
||||
render() {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
// Handler is removed after the first click
|
||||
onClick={this.state.disabled ? null : this.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
expect(Scheduler).toFlushAndYield(['render button: enabled']);
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {disabled: false};
|
||||
onClick = () => {
|
||||
// Perform some side-effect
|
||||
Scheduler.unstable_yieldValue('Side-effect');
|
||||
// Disable the button
|
||||
this.setState({disabled: true});
|
||||
};
|
||||
render() {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
// Handler is removed after the first click
|
||||
onClick={this.state.disabled ? null : this.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Click the button to trigger the side-effect
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// The handler fired
|
||||
'Side-effect',
|
||||
// but the component did not re-render yet, because it's async
|
||||
]);
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
expect(Scheduler).toFlushAndYield(['render button: enabled']);
|
||||
|
||||
// Click the button again
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Before handling this second click event, the previous interactive
|
||||
// update is flushed
|
||||
'render button: disabled',
|
||||
// The event handler was removed from the button, so there's no second
|
||||
// side-effect
|
||||
]);
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
|
||||
// The handler should not fire again no matter how many times we
|
||||
// click the handler.
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
},
|
||||
);
|
||||
// Click the button to trigger the side-effect
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// The handler fired
|
||||
'Side-effect',
|
||||
// but the component did not re-render yet, because it's async
|
||||
]);
|
||||
|
||||
it.experimental(
|
||||
'end result of many interactive updates is deterministic',
|
||||
() => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
// Click the button again
|
||||
click();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Before handling this second click event, the previous interactive
|
||||
// update is flushed
|
||||
'render button: disabled',
|
||||
// The event handler was removed from the button, so there's no second
|
||||
// side-effect
|
||||
]);
|
||||
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {count: 0};
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() =>
|
||||
// Intentionally not using the updater form here
|
||||
this.setState({count: this.state.count + 1})
|
||||
}>
|
||||
Count: {this.state.count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
// The handler should not fire again no matter how many times we
|
||||
// click the handler.
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
});
|
||||
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
// @gate experimental
|
||||
it('end result of many interactive updates is deterministic', () => {
|
||||
container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
let button;
|
||||
class Button extends React.Component {
|
||||
state = {count: 0};
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
ref={el => (button = el)}
|
||||
onClick={() =>
|
||||
// Intentionally not using the updater form here
|
||||
this.setState({count: this.state.count + 1})
|
||||
}>
|
||||
Count: {this.state.count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Click the button a single time
|
||||
click();
|
||||
// The counter should not have updated yet because it's async
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
// Initial mount
|
||||
root.render(<Button />);
|
||||
// Should not have flushed yet because it's async
|
||||
expect(button).toBe(undefined);
|
||||
// Flush async work
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
function click() {
|
||||
button.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
|
||||
// Flush the remaining work
|
||||
Scheduler.unstable_flushAll();
|
||||
// The counter should equal the total number of clicks
|
||||
expect(button.textContent).toEqual('Count: 7');
|
||||
},
|
||||
);
|
||||
// Click the button a single time
|
||||
click();
|
||||
// The counter should not have updated yet because it's async
|
||||
expect(button.textContent).toEqual('Count: 0');
|
||||
|
||||
it.experimental('flushes discrete updates in order', () => {
|
||||
// Click the button many more times
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
|
||||
// Flush the remaining work
|
||||
Scheduler.unstable_flushAll();
|
||||
// The counter should equal the total number of clicks
|
||||
expect(button.textContent).toEqual('Count: 7');
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('flushes discrete updates in order', () => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ describe('ReactFlightDOMRelay', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.experimental('can transfer a Block to the client and render there', () => {
|
||||
// @gate experimental
|
||||
it('can transfer a Block to the client and render there', () => {
|
||||
function load(firstName, lastName) {
|
||||
return {name: firstName + ' ' + lastName};
|
||||
}
|
||||
|
||||
@@ -140,7 +140,8 @@ describe('ReactFlightDOM', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.experimental('should resolve the root', async () => {
|
||||
// @gate experimental
|
||||
it('should resolve the root', async () => {
|
||||
const {Suspense} = React;
|
||||
|
||||
// Model
|
||||
@@ -191,7 +192,8 @@ describe('ReactFlightDOM', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.experimental('should not get confused by $', async () => {
|
||||
// @gate experimental
|
||||
it('should not get confused by $', async () => {
|
||||
const {Suspense} = React;
|
||||
|
||||
// Model
|
||||
@@ -227,7 +229,8 @@ describe('ReactFlightDOM', () => {
|
||||
expect(container.innerHTML).toBe('<p>$1</p>');
|
||||
});
|
||||
|
||||
it.experimental('should not get confused by @', async () => {
|
||||
// @gate experimental
|
||||
it('should not get confused by @', async () => {
|
||||
const {Suspense} = React;
|
||||
|
||||
// Model
|
||||
@@ -263,7 +266,8 @@ describe('ReactFlightDOM', () => {
|
||||
expect(container.innerHTML).toBe('<p>@div</p>');
|
||||
});
|
||||
|
||||
it.experimental('should progressively reveal Blocks', async () => {
|
||||
// @gate experimental
|
||||
it('should progressively reveal Blocks', async () => {
|
||||
const {Suspense} = React;
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
||||
@@ -344,69 +344,65 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
|
||||
);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'is called after a focused suspended element is hidden',
|
||||
() => {
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(
|
||||
resolvePromise => (resolve = resolvePromise),
|
||||
);
|
||||
// @gate experimental
|
||||
it('is called after a focused suspended element is hidden', () => {
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Component = ({show}) => {
|
||||
const listener = useFocusWithin({
|
||||
onBeforeBlurWithin,
|
||||
onBlurWithin,
|
||||
});
|
||||
const Component = ({show}) => {
|
||||
const listener = useFocusWithin({
|
||||
onBeforeBlurWithin,
|
||||
onBlurWithin,
|
||||
});
|
||||
|
||||
return (
|
||||
<div DEPRECATED_flareListeners={listener}>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const container2 = document.createElement('div');
|
||||
document.body.appendChild(container2);
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
root.render(<Component />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe('<div><input></div>');
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
root.render(<Component />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe(
|
||||
'<div><input style="display: none;">Loading...</div>',
|
||||
return (
|
||||
<div DEPRECATED_flareListeners={listener}>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onBlurWithin).toHaveBeenCalledTimes(1);
|
||||
resolve();
|
||||
};
|
||||
|
||||
document.body.removeChild(container2);
|
||||
},
|
||||
);
|
||||
const container2 = document.createElement('div');
|
||||
document.body.appendChild(container2);
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
root.render(<Component />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe('<div><input></div>');
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
root.render(<Component />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe(
|
||||
'<div><input style="display: none;">Loading...</div>',
|
||||
);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onBlurWithin).toHaveBeenCalledTimes(1);
|
||||
resolve();
|
||||
|
||||
document.body.removeChild(container2);
|
||||
});
|
||||
});
|
||||
|
||||
it('expect displayName to show up for event component', () => {
|
||||
|
||||
@@ -793,7 +793,8 @@ describe('Input event responder', () => {
|
||||
});
|
||||
|
||||
describe('concurrent mode', () => {
|
||||
it.experimental('text input', () => {
|
||||
// @gate experimental
|
||||
it('text input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -848,7 +849,8 @@ describe('Input event responder', () => {
|
||||
expect(input.value).toBe('changed [!]');
|
||||
});
|
||||
|
||||
it.experimental('checkbox input', () => {
|
||||
// @gate experimental
|
||||
it('checkbox input', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let input;
|
||||
|
||||
@@ -916,7 +918,8 @@ describe('Input event responder', () => {
|
||||
expect(input.checked).toBe(false);
|
||||
});
|
||||
|
||||
it.experimental('textarea', () => {
|
||||
// @gate experimental
|
||||
it('textarea', () => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
let textarea;
|
||||
|
||||
|
||||
+97
-98
@@ -49,7 +49,8 @@ describe('ReactBlocks', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it.experimental('renders a simple component', () => {
|
||||
// @gate experimental
|
||||
it('renders a simple component', () => {
|
||||
function User(props, data) {
|
||||
return <div>{typeof data}</div>;
|
||||
}
|
||||
@@ -71,7 +72,8 @@ describe('ReactBlocks', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>undefined</div>);
|
||||
});
|
||||
|
||||
it.experimental('prints the name of the render function in warnings', () => {
|
||||
// @gate experimental
|
||||
it('prints the name of the render function in warnings', () => {
|
||||
function load(firstName) {
|
||||
return {
|
||||
name: firstName,
|
||||
@@ -108,7 +110,8 @@ describe('ReactBlocks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.experimental('renders a component with a suspending load', async () => {
|
||||
// @gate experimental
|
||||
it('renders a component with a suspending load', async () => {
|
||||
function load(id) {
|
||||
return {
|
||||
id: id,
|
||||
@@ -147,115 +150,111 @@ describe('ReactBlocks', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span>Name: Sebastian</span>);
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'does not support a lazy wrapper around a chunk',
|
||||
async () => {
|
||||
function load(id) {
|
||||
return {
|
||||
id: id,
|
||||
name: readString('Sebastian'),
|
||||
};
|
||||
}
|
||||
// @gate experimental
|
||||
it('does not support a lazy wrapper around a chunk', async () => {
|
||||
function load(id) {
|
||||
return {
|
||||
id: id,
|
||||
name: readString('Sebastian'),
|
||||
};
|
||||
}
|
||||
|
||||
function Render(props, data) {
|
||||
return (
|
||||
<span>
|
||||
{props.title}: {data.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const loadUser = block(Render, load);
|
||||
|
||||
function App({User}) {
|
||||
return (
|
||||
<Suspense fallback={'Loading...'}>
|
||||
<User title="Name" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
let resolveLazy;
|
||||
const LazyUser = React.lazy(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveLazy = function() {
|
||||
resolve({
|
||||
default: loadUser(123),
|
||||
});
|
||||
};
|
||||
}),
|
||||
function Render(props, data) {
|
||||
return (
|
||||
<span>
|
||||
{props.title}: {data.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={LazyUser} />);
|
||||
});
|
||||
const loadUser = block(Render, load);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
|
||||
// Resolve the component.
|
||||
await resolveLazy();
|
||||
|
||||
expect(Scheduler).toFlushAndThrow(
|
||||
'Element type is invalid. Received a promise that resolves to: [object Object]. ' +
|
||||
'Lazy element type must resolve to a class or function.' +
|
||||
(__DEV__
|
||||
? ' Did you wrap a component in React.lazy() more than once?'
|
||||
: ''),
|
||||
function App({User}) {
|
||||
return (
|
||||
<Suspense fallback={'Loading...'}>
|
||||
<User title="Name" />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
it.experimental(
|
||||
'can receive updated data for the same component',
|
||||
async () => {
|
||||
function load(firstName) {
|
||||
return {
|
||||
name: firstName,
|
||||
};
|
||||
}
|
||||
let resolveLazy;
|
||||
const LazyUser = React.lazy(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveLazy = function() {
|
||||
resolve({
|
||||
default: loadUser(123),
|
||||
});
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
function Render(props, data) {
|
||||
const [initialName] = useState(data.name);
|
||||
return (
|
||||
<>
|
||||
<span>Initial name: {initialName}</span>
|
||||
<span>Latest name: {data.name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={LazyUser} />);
|
||||
});
|
||||
|
||||
const loadUser = block(Render, load);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
|
||||
function App({User}) {
|
||||
return (
|
||||
<Suspense fallback={'Loading...'}>
|
||||
<User title="Name" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
// Resolve the component.
|
||||
await resolveLazy();
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={loadUser('Sebastian')} />);
|
||||
});
|
||||
expect(Scheduler).toFlushAndThrow(
|
||||
'Element type is invalid. Received a promise that resolves to: [object Object]. ' +
|
||||
'Lazy element type must resolve to a class or function.' +
|
||||
(__DEV__
|
||||
? ' Did you wrap a component in React.lazy() more than once?'
|
||||
: ''),
|
||||
);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
// @gate experimental
|
||||
it('can receive updated data for the same component', async () => {
|
||||
function load(firstName) {
|
||||
return {
|
||||
name: firstName,
|
||||
};
|
||||
}
|
||||
|
||||
function Render(props, data) {
|
||||
const [initialName] = useState(data.name);
|
||||
return (
|
||||
<>
|
||||
<span>Initial name: Sebastian</span>
|
||||
<span>Latest name: Sebastian</span>
|
||||
</>,
|
||||
<span>Initial name: {initialName}</span>
|
||||
<span>Latest name: {data.name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={loadUser('Dan')} />);
|
||||
});
|
||||
const loadUser = block(Render, load);
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span>Initial name: Sebastian</span>
|
||||
<span>Latest name: Dan</span>
|
||||
</>,
|
||||
function App({User}) {
|
||||
return (
|
||||
<Suspense fallback={'Loading...'}>
|
||||
<User title="Name" />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={loadUser('Sebastian')} />);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span>Initial name: Sebastian</span>
|
||||
<span>Latest name: Sebastian</span>
|
||||
</>,
|
||||
);
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.render(<App User={loadUser('Dan')} />);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span>Initial name: Sebastian</span>
|
||||
<span>Latest name: Dan</span>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+136
-138
@@ -798,7 +798,8 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
|
||||
// TODO: This should probably warn
|
||||
it.experimental('calling startTransition inside render phase', async () => {
|
||||
// @gate experimental
|
||||
it('calling startTransition inside render phase', async () => {
|
||||
let startTransition;
|
||||
function App() {
|
||||
const [counter, setCounter] = useState(0);
|
||||
@@ -2728,152 +2729,149 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
describe('useTransition', () => {
|
||||
it.experimental(
|
||||
'delays showing loading state until after timeout',
|
||||
async () => {
|
||||
let transition;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition({
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
transition = () => {
|
||||
startTransition(() => {
|
||||
setShow(true);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
|
||||
{show ? (
|
||||
<AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
|
||||
) : (
|
||||
<Text text={`Before... Pending: ${isPending}`} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactNoop.render(<App />);
|
||||
expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: false'),
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
Scheduler.unstable_runWithPriority(
|
||||
Scheduler.unstable_UserBlockingPriority,
|
||||
transition,
|
||||
);
|
||||
// @gate experimental
|
||||
it('delays showing loading state until after timeout', async () => {
|
||||
let transition;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition({
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
Scheduler.unstable_advanceTime(500);
|
||||
await advanceTimers(500);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Before... Pending: true',
|
||||
'Suspend! [After... Pending: false]',
|
||||
'Loading... Pending: false',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
hiddenSpan('Before... Pending: true'),
|
||||
span('Loading... Pending: false'),
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(500);
|
||||
await advanceTimers(500);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [After... Pending: false]',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield(['After... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('After... Pending: false'),
|
||||
]);
|
||||
},
|
||||
);
|
||||
it.experimental(
|
||||
'delays showing loading state until after busyDelayMs + busyMinDurationMs',
|
||||
async () => {
|
||||
let transition;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition({
|
||||
busyDelayMs: 1000,
|
||||
busyMinDurationMs: 2000,
|
||||
transition = () => {
|
||||
startTransition(() => {
|
||||
setShow(true);
|
||||
});
|
||||
transition = () => {
|
||||
startTransition(() => {
|
||||
setShow(true);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
|
||||
{show ? (
|
||||
<AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
|
||||
) : (
|
||||
<Text text={`Before... Pending: ${isPending}`} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactNoop.render(<App />);
|
||||
expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: false'),
|
||||
]);
|
||||
};
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
|
||||
{show ? (
|
||||
<AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
|
||||
) : (
|
||||
<Text text={`Before... Pending: ${isPending}`} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactNoop.render(<App />);
|
||||
expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: false'),
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
Scheduler.unstable_runWithPriority(
|
||||
Scheduler.unstable_UserBlockingPriority,
|
||||
transition,
|
||||
);
|
||||
act(() => {
|
||||
Scheduler.unstable_runWithPriority(
|
||||
Scheduler.unstable_UserBlockingPriority,
|
||||
transition,
|
||||
);
|
||||
});
|
||||
Scheduler.unstable_advanceTime(500);
|
||||
await advanceTimers(500);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Before... Pending: true',
|
||||
'Suspend! [After... Pending: false]',
|
||||
'Loading... Pending: false',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
hiddenSpan('Before... Pending: true'),
|
||||
span('Loading... Pending: false'),
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(500);
|
||||
await advanceTimers(500);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [After... Pending: false]',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield(['After... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('After... Pending: false'),
|
||||
]);
|
||||
});
|
||||
// @gate experimental
|
||||
it('delays showing loading state until after busyDelayMs + busyMinDurationMs', async () => {
|
||||
let transition;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition({
|
||||
busyDelayMs: 1000,
|
||||
busyMinDurationMs: 2000,
|
||||
});
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Before... Pending: true',
|
||||
'Suspend! [After... Pending: false]',
|
||||
'Loading... Pending: false',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
transition = () => {
|
||||
startTransition(() => {
|
||||
setShow(true);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
|
||||
{show ? (
|
||||
<AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
|
||||
) : (
|
||||
<Text text={`Before... Pending: ${isPending}`} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
ReactNoop.render(<App />);
|
||||
expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: false'),
|
||||
]);
|
||||
|
||||
// Resolve the promise. The whole tree has now completed. However,
|
||||
// because we exceeded the busy threshold, we won't commit the
|
||||
// result yet.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [After... Pending: false]',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield(['After... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
act(() => {
|
||||
Scheduler.unstable_runWithPriority(
|
||||
Scheduler.unstable_UserBlockingPriority,
|
||||
transition,
|
||||
);
|
||||
});
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Before... Pending: true',
|
||||
'Suspend! [After... Pending: false]',
|
||||
'Loading... Pending: false',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
// Advance time until just before the `busyMinDuration` threshold.
|
||||
Scheduler.unstable_advanceTime(999);
|
||||
await advanceTimers(999);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
// Resolve the promise. The whole tree has now completed. However,
|
||||
// because we exceeded the busy threshold, we won't commit the
|
||||
// result yet.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [After... Pending: false]',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield(['After... Pending: false']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
// Advance time just a bit more. Now we complete the transition.
|
||||
Scheduler.unstable_advanceTime(300);
|
||||
await advanceTimers(300);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('After... Pending: false'),
|
||||
]);
|
||||
},
|
||||
);
|
||||
// Advance time until just before the `busyMinDuration` threshold.
|
||||
Scheduler.unstable_advanceTime(999);
|
||||
await advanceTimers(999);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
// Advance time just a bit more. Now we complete the transition.
|
||||
Scheduler.unstable_advanceTime(300);
|
||||
await advanceTimers(300);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('After... Pending: false'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('useDeferredValue', () => {
|
||||
it.experimental('defers text value until specified timeout', async () => {
|
||||
// @gate experimental
|
||||
it('defers text value until specified timeout', async () => {
|
||||
function TextBox({text}) {
|
||||
return <AsyncText ms={1000} text={text} />;
|
||||
}
|
||||
|
||||
+106
-108
@@ -3738,127 +3738,125 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
});
|
||||
|
||||
// Regression: https://github.com/facebook/react/issues/18486
|
||||
it.experimental(
|
||||
'does not get stuck in pending state with render phase updates',
|
||||
async () => {
|
||||
let setTextWithShortTransition;
|
||||
let setTextWithLongTransition;
|
||||
// @gate experimental
|
||||
it('does not get stuck in pending state with render phase updates', async () => {
|
||||
let setTextWithShortTransition;
|
||||
let setTextWithLongTransition;
|
||||
|
||||
function App() {
|
||||
const [startShortTransition, isPending1] = React.useTransition({
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
const [startLongTransition, isPending2] = React.useTransition({
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
const isPending = isPending1 || isPending2;
|
||||
const [text, setText] = React.useState('');
|
||||
const [mirror, setMirror] = React.useState('');
|
||||
function App() {
|
||||
const [startShortTransition, isPending1] = React.useTransition({
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
const [startLongTransition, isPending2] = React.useTransition({
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
const isPending = isPending1 || isPending2;
|
||||
const [text, setText] = React.useState('');
|
||||
const [mirror, setMirror] = React.useState('');
|
||||
|
||||
if (text !== mirror) {
|
||||
// Render phase update was needed to repro the bug.
|
||||
setMirror(text);
|
||||
}
|
||||
|
||||
setTextWithShortTransition = value => {
|
||||
startShortTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
setTextWithLongTransition = value => {
|
||||
startLongTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPending ? <Text text="Pending..." /> : null}
|
||||
{text !== '' ? <AsyncText text={text} /> : <Text text={text} />}
|
||||
</>
|
||||
);
|
||||
if (text !== mirror) {
|
||||
// Render phase update was needed to repro the bug.
|
||||
setMirror(text);
|
||||
}
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
setTextWithShortTransition = value => {
|
||||
startShortTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
setTextWithLongTransition = value => {
|
||||
startLongTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<Root />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['']);
|
||||
expect(root).toMatchRenderedOutput(<span prop="" />);
|
||||
|
||||
// Update to "a". That will suspend.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithShortTransition('a');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Pending...',
|
||||
'',
|
||||
'Suspend! [a]',
|
||||
'Loading...',
|
||||
]);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
return (
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="" />
|
||||
</>,
|
||||
{isPending ? <Text text="Pending..." /> : null}
|
||||
{text !== '' ? <AsyncText text={text} /> : <Text text={text} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Update to "b". That will suspend, too.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithLongTransition('b');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Neither is resolved yet.
|
||||
'Pending...',
|
||||
'',
|
||||
'Pending...',
|
||||
'Suspend! [a]',
|
||||
'Loading...',
|
||||
'Suspend! [b]',
|
||||
'Loading...',
|
||||
]);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="" />
|
||||
</>,
|
||||
function Root() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve "a". But "b" is still pending.
|
||||
await ReactNoop.act(async () => {
|
||||
await resolveText('a');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [a]',
|
||||
const root = ReactNoop.createRoot();
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<Root />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['']);
|
||||
expect(root).toMatchRenderedOutput(<span prop="" />);
|
||||
|
||||
// Update to "a". That will suspend.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithShortTransition('a');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Pending...',
|
||||
'a',
|
||||
'',
|
||||
'Suspend! [a]',
|
||||
'Loading...',
|
||||
]);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Update to "b". That will suspend, too.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithLongTransition('b');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Neither is resolved yet.
|
||||
'Pending...',
|
||||
'',
|
||||
'Pending...',
|
||||
'Suspend! [a]',
|
||||
'Loading...',
|
||||
'Suspend! [b]',
|
||||
'Loading...',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="a" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Resolve "b". This should remove the pending state.
|
||||
await ReactNoop.act(async () => {
|
||||
await resolveText('b');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']);
|
||||
// The bug was that the pending state got stuck forever.
|
||||
expect(root).toMatchRenderedOutput(<span prop="b" />);
|
||||
},
|
||||
);
|
||||
// Resolve "a". But "b" is still pending.
|
||||
await ReactNoop.act(async () => {
|
||||
await resolveText('a');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [a]',
|
||||
'Pending...',
|
||||
'a',
|
||||
'Suspend! [b]',
|
||||
'Loading...',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Pending..." />
|
||||
<span prop="a" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Resolve "b". This should remove the pending state.
|
||||
await ReactNoop.act(async () => {
|
||||
await resolveText('b');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']);
|
||||
// The bug was that the pending state got stuck forever.
|
||||
expect(root).toMatchRenderedOutput(<span prop="b" />);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,47 +59,45 @@ describe('ReactTransition', () => {
|
||||
return Component;
|
||||
}
|
||||
|
||||
it.experimental(
|
||||
'isPending works even if called from outside an input event',
|
||||
async () => {
|
||||
const Async = createAsyncText('Async');
|
||||
let start;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition();
|
||||
start = () => startTransition(() => setShow(true));
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
{isPending ? <Text text="Pending..." /> : null}
|
||||
{show ? <Async /> : <Text text="(empty)" />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
// @gate experimental
|
||||
it('isPending works even if called from outside an input event', async () => {
|
||||
const Async = createAsyncText('Async');
|
||||
let start;
|
||||
function App() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [startTransition, isPending] = useTransition();
|
||||
start = () => startTransition(() => setShow(true));
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
{isPending ? <Text text="Pending..." /> : null}
|
||||
{show ? <Async /> : <Text text="(empty)" />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['(empty)']);
|
||||
expect(root).toMatchRenderedOutput('(empty)');
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['(empty)']);
|
||||
expect(root).toMatchRenderedOutput('(empty)');
|
||||
|
||||
await act(async () => {
|
||||
start();
|
||||
await act(async () => {
|
||||
start();
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Pending...',
|
||||
'(empty)',
|
||||
'Suspend! [Async]',
|
||||
'Loading...',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Pending...',
|
||||
'(empty)',
|
||||
'Suspend! [Async]',
|
||||
'Loading...',
|
||||
]);
|
||||
|
||||
expect(root).toMatchRenderedOutput('Pending...(empty)');
|
||||
expect(root).toMatchRenderedOutput('Pending...(empty)');
|
||||
|
||||
await Async.resolve();
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Async']);
|
||||
expect(root).toMatchRenderedOutput('Async');
|
||||
},
|
||||
);
|
||||
await Async.resolve();
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Async']);
|
||||
expect(root).toMatchRenderedOutput('Async');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -362,76 +362,75 @@ describe('Concurrent Mode', () => {
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'should warn about unsafe legacy lifecycle methods anywhere in the tree',
|
||||
() => {
|
||||
class AsyncRoot extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
UNSAFE_componentWillUpdate() {}
|
||||
render() {
|
||||
return (
|
||||
// @gate experimental
|
||||
it('should warn about unsafe legacy lifecycle methods anywhere in the tree', () => {
|
||||
class AsyncRoot extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
UNSAFE_componentWillUpdate() {}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Wrapper>
|
||||
<Foo />
|
||||
</Wrapper>
|
||||
<div>
|
||||
<Wrapper>
|
||||
<Foo />
|
||||
</Wrapper>
|
||||
<div>
|
||||
<Bar />
|
||||
<Foo />
|
||||
</div>
|
||||
<Bar />
|
||||
<Foo />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Wrapper({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
function Wrapper({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
class Foo extends React.Component {
|
||||
UNSAFE_componentWillReceiveProps() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
class Foo extends React.Component {
|
||||
UNSAFE_componentWillReceiveProps() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class Bar extends React.Component {
|
||||
UNSAFE_componentWillReceiveProps() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class Bar extends React.Component {
|
||||
UNSAFE_componentWillReceiveProps() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<AsyncRoot />);
|
||||
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
|
||||
[
|
||||
/* eslint-disable max-len */
|
||||
`Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<AsyncRoot />);
|
||||
expect(() => Scheduler.unstable_flushAll()).toErrorDev(
|
||||
[
|
||||
/* eslint-disable max-len */
|
||||
`Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
|
||||
* Move code with side effects to componentDidMount, and set initial state in the constructor.
|
||||
|
||||
Please update the following components: AsyncRoot`,
|
||||
`Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
`Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
|
||||
* Move data fetching code or side effects to componentDidUpdate.
|
||||
* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state
|
||||
|
||||
Please update the following components: Bar, Foo`,
|
||||
`Warning: Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
`Warning: Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
|
||||
|
||||
* Move data fetching code or side effects to componentDidUpdate.
|
||||
|
||||
Please update the following components: AsyncRoot`,
|
||||
/* eslint-enable max-len */
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
/* eslint-enable max-len */
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// Dedupe
|
||||
root.render(<AsyncRoot />);
|
||||
Scheduler.unstable_flushAll();
|
||||
},
|
||||
);
|
||||
// Dedupe
|
||||
root.render(<AsyncRoot />);
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
it.experimental('should coalesce warnings by lifecycle name', () => {
|
||||
// @gate experimental
|
||||
it('should coalesce warnings by lifecycle name', () => {
|
||||
class AsyncRoot extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
UNSAFE_componentWillUpdate() {}
|
||||
@@ -513,52 +512,50 @@ Please update the following components: Parent`,
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
it.experimental(
|
||||
'should warn about components not present during the initial render',
|
||||
() => {
|
||||
class AsyncRoot extends React.Component {
|
||||
render() {
|
||||
return this.props.foo ? <Foo /> : <Bar />;
|
||||
}
|
||||
// @gate experimental
|
||||
it('should warn about components not present during the initial render', () => {
|
||||
class AsyncRoot extends React.Component {
|
||||
render() {
|
||||
return this.props.foo ? <Foo /> : <Bar />;
|
||||
}
|
||||
class Foo extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class Foo extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
class Bar extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class Bar extends React.Component {
|
||||
UNSAFE_componentWillMount() {}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<AsyncRoot foo={true} />);
|
||||
expect(() =>
|
||||
Scheduler.unstable_flushAll(),
|
||||
).toErrorDev(
|
||||
'Using UNSAFE_componentWillMount in strict mode is not recommended',
|
||||
{withoutStack: true},
|
||||
);
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<AsyncRoot foo={true} />);
|
||||
expect(() =>
|
||||
Scheduler.unstable_flushAll(),
|
||||
).toErrorDev(
|
||||
'Using UNSAFE_componentWillMount in strict mode is not recommended',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
root.render(<AsyncRoot foo={false} />);
|
||||
expect(() =>
|
||||
Scheduler.unstable_flushAll(),
|
||||
).toErrorDev(
|
||||
'Using UNSAFE_componentWillMount in strict mode is not recommended',
|
||||
{withoutStack: true},
|
||||
);
|
||||
root.render(<AsyncRoot foo={false} />);
|
||||
expect(() =>
|
||||
Scheduler.unstable_flushAll(),
|
||||
).toErrorDev(
|
||||
'Using UNSAFE_componentWillMount in strict mode is not recommended',
|
||||
{withoutStack: true},
|
||||
);
|
||||
|
||||
// Dedupe
|
||||
root.render(<AsyncRoot foo={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(<AsyncRoot foo={false} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
},
|
||||
);
|
||||
// Dedupe
|
||||
root.render(<AsyncRoot foo={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
root.render(<AsyncRoot foo={false} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
it('should also warn inside of "strict" mode trees', () => {
|
||||
const {StrictMode} = React;
|
||||
|
||||
@@ -267,37 +267,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
||||
throw Error(errorMsg);
|
||||
};
|
||||
|
||||
// TODO: Deprecate these helpers in favor of @gate pragma
|
||||
const it = global.it;
|
||||
const fit = global.fit;
|
||||
const xit = global.xit;
|
||||
if (__EXPERIMENTAL__) {
|
||||
it.experimental = it;
|
||||
fit.experimental = it.only.experimental = it.experimental.only = fit;
|
||||
xit.experimental = it.skip.experimental = it.experimental.skip = xit;
|
||||
} else {
|
||||
const errorMessage =
|
||||
'Tests marked experimental are expected to fail, but this one passed.';
|
||||
it.experimental = (message, callback) => {
|
||||
it(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
|
||||
expectTestToFail(callback, errorMessage));
|
||||
};
|
||||
fit.experimental = it.only.experimental = it.experimental.only = (
|
||||
message,
|
||||
callback
|
||||
) => {
|
||||
fit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
|
||||
expectTestToFail(callback, errorMessage));
|
||||
};
|
||||
xit.experimental = it.skip.experimental = it.experimental.skip = (
|
||||
message,
|
||||
callback
|
||||
) => {
|
||||
xit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
|
||||
expectTestToFail(callback, errorMessage));
|
||||
};
|
||||
}
|
||||
|
||||
const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
|
||||
global._test_gate = (gateFn, testName, callback) => {
|
||||
let shouldPass;
|
||||
|
||||
Reference in New Issue
Block a user