[react-events] Split out mixed event responder tests (#16608)

This commit is contained in:
Dominic Gannaway
2019-08-29 16:13:37 +01:00
committed by GitHub
parent f61138e068
commit 4e544cffee
5 changed files with 403 additions and 419 deletions
@@ -994,4 +994,68 @@ describe('DOMEventResponderSystem', () => {
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 1}]);
});
it('should correctly pass through event properties', () => {
const timeStamps = [];
const ref = React.createRef();
const eventLog = [];
const logEvent = event => {
const propertiesWeCareAbout = {
counter: event.counter,
target: event.target,
timeStamp: event.timeStamp,
type: event.type,
};
timeStamps.push(event.timeStamp);
eventLog.push(propertiesWeCareAbout);
};
let counter = 0;
const TestResponder = createEventResponder({
targetEventTypes: ['click'],
onEvent(event, context, props) {
const obj = {
counter,
timeStamp: context.getTimeStamp(),
target: event.responderTarget,
type: 'click-test',
};
context.dispatchEvent(obj, props.onClick, DiscreteEvent);
},
});
const Component = () => {
const listener = React.unstable_useResponder(TestResponder, {
onClick: logEvent,
});
return <button ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
dispatchClickEvent(ref.current);
counter++;
dispatchClickEvent(ref.current);
counter++;
dispatchClickEvent(ref.current);
expect(typeof timeStamps[0] === 'number').toBe(true);
expect(eventLog).toEqual([
{
counter: 0,
target: ref.current,
timeStamp: timeStamps[0],
type: 'click-test',
},
{
counter: 1,
target: ref.current,
timeStamp: timeStamps[1],
type: 'click-test',
},
{
counter: 2,
target: ref.current,
timeStamp: timeStamps[2],
type: 'click-test',
},
]);
});
});
@@ -14,7 +14,6 @@ let ReactFeatureFlags;
let ReactDOM;
let InputResponder;
let useInput;
let usePress;
let Scheduler;
const setUntrackedChecked = Object.getOwnPropertyDescriptor(
@@ -42,7 +41,6 @@ const modulesInit = () => {
Scheduler = require('scheduler');
InputResponder = require('react-events/input').InputResponder;
useInput = require('react-events/input').useInput;
usePress = require('react-events/press').usePress;
};
describe('Input event responder', () => {
@@ -948,96 +946,6 @@ describe('Input event responder', () => {
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
it('is async for non-input events', () => {
const root = ReactDOM.unstable_createRoot(container);
let input;
let ops = [];
function Component({
innerRef,
onChange,
controlledValue,
pressListener,
}) {
const inputListener = useInput({
onChange,
});
return (
<input
type="text"
ref={innerRef}
value={controlledValue}
listeners={[inputListener, pressListener]}
/>
);
}
function PressWrapper({innerRef, onPress, onChange, controlledValue}) {
const pressListener = usePress({
onPress,
});
return (
<Component
onChange={onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
pressListener={pressListener}
/>
);
}
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
reset = () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<PressWrapper
onPress={this.reset}
onChange={this.onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a click event
input.dispatchEvent(
new MouseEvent('mousedown', {bubbles: true, cancelable: true}),
);
input.dispatchEvent(
new MouseEvent('mouseup', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(input.value).toBe('initial');
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');
});
});
});
@@ -0,0 +1,326 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {createEventTarget} from '../testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let Scheduler;
describe('mixing responders with the heritage event system', () => {
let container;
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should properly only flush sync once when the event systems are mixed', () => {
const usePress = require('react-events/press').usePress;
const ref = React.createRef();
let renderCounts = 0;
function MyComponent() {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
ref={ref}
listeners={listener}
onClick={() => {
updateCounter(count => count + 1);
}}>
Press me
</button>
</div>
);
}
const newContainer = document.createElement('div');
const root = ReactDOM.unstable_createRoot(newContainer);
document.body.appendChild(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
const target = createEventTarget(ref.current);
target.pointerdown({timeStamp: 100});
target.pointerup({timeStamp: 100});
target.click({timeStamp: 100});
if (__DEV__) {
expect(renderCounts).toBe(2);
} else {
expect(renderCounts).toBe(1);
}
Scheduler.unstable_flushAll();
if (__DEV__) {
expect(renderCounts).toBe(4);
} else {
expect(renderCounts).toBe(2);
}
target.pointerdown({timeStamp: 100});
target.pointerup({timeStamp: 100});
// Ensure the timeStamp logic works
target.click({timeStamp: 101});
if (__DEV__) {
expect(renderCounts).toBe(6);
} else {
expect(renderCounts).toBe(3);
}
Scheduler.unstable_flushAll();
document.body.removeChild(newContainer);
});
it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
const usePress = require('react-events/press').usePress;
const ref = React.createRef();
let renderCounts = 0;
function MyComponent() {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
ref={ref}
listeners={listener}
onClick={() => {
// This should flush synchronously
ReactDOM.unstable_flushDiscreteUpdates();
updateCounter(count => count + 1);
}}>
Press me
</button>
</div>
);
}
const newContainer = document.createElement('div');
const root = ReactDOM.unstable_createRoot(newContainer);
document.body.appendChild(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
const target = createEventTarget(ref.current);
target.pointerdown({timeStamp: 100});
target.pointerup({timeStamp: 100});
if (__DEV__) {
expect(renderCounts).toBe(4);
} else {
expect(renderCounts).toBe(2);
}
Scheduler.unstable_flushAll();
if (__DEV__) {
expect(renderCounts).toBe(6);
} else {
expect(renderCounts).toBe(3);
}
target.pointerdown({timeStamp: 100});
// Ensure the timeStamp logic works
target.pointerup({timeStamp: 101});
if (__DEV__) {
expect(renderCounts).toBe(8);
} else {
expect(renderCounts).toBe(4);
}
Scheduler.unstable_flushAll();
document.body.removeChild(newContainer);
});
it(
'should only flush before outermost discrete event handler when mixing ' +
'event systems',
async () => {
const {useState} = React;
const usePress = require('react-events/press').usePress;
const button = React.createRef();
const ops = [];
function MyComponent() {
const [pressesCount, updatePressesCount] = useState(0);
const [clicksCount, updateClicksCount] = useState(0);
function handlePress() {
// This dispatches a synchronous, discrete event in the legacy event
// system. However, because it's nested inside the new event system,
// its updates should not flush until the end of the outer handler.
const target = createEventTarget(button.current);
target.click();
// Text context should not have changed
ops.push(newContainer.textContent);
updatePressesCount(pressesCount + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
listeners={listener}
ref={button}
onClick={() => updateClicksCount(clicksCount + 1)}>
Presses: {pressesCount}, Clicks: {clicksCount}
</button>
</div>
);
}
const newContainer = document.createElement('div');
document.body.appendChild(newContainer);
const root = ReactDOM.unstable_createRoot(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0');
const target = createEventTarget(button.current);
target.pointerdown({timeStamp: 100});
target.pointerup({timeStamp: 100});
Scheduler.unstable_flushAll();
expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1');
expect(ops).toEqual(['Presses: 0, Clicks: 0']);
},
);
describe('mixing the Input and Press repsonders', () => {
it('is async for non-input events', () => {
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableUserBlockingEvents = true;
const usePress = require('react-events/press').usePress;
const useInput = require('react-events/input').useInput;
const root = ReactDOM.unstable_createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue, pressListener}) {
const inputListener = useInput({
onChange,
});
return (
<input
type="text"
ref={innerRef}
value={controlledValue}
listeners={[inputListener, pressListener]}
/>
);
}
function PressWrapper({innerRef, onPress, onChange, controlledValue}) {
const pressListener = usePress({
onPress,
});
return (
<Component
onChange={onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
pressListener={pressListener}
/>
);
}
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
reset = () => {
this.setState({value: ''});
};
render() {
ops.push(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<PressWrapper
onPress={this.reset}
onChange={this.onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
Scheduler.unstable_flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
ops = [];
// Trigger a click event
input.dispatchEvent(
new MouseEvent('mousedown', {bubbles: true, cancelable: true}),
);
input.dispatchEvent(
new MouseEvent('mouseup', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(ops).toEqual([]);
expect(input.value).toBe('initial');
// Flush callbacks.
Scheduler.unstable_flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');
});
});
});
@@ -1120,334 +1120,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
target.pointerdown();
});
it('should correctly pass through event properties', () => {
const timeStamps = [];
const ref = React.createRef();
const eventLog = [];
const logEvent = event => {
const propertiesWeCareAbout = {
pageX: event.pageX,
pageY: event.pageY,
screenX: event.screenX,
screenY: event.screenY,
clientX: event.clientX,
clientY: event.clientY,
pointerType: event.pointerType,
target: event.target,
timeStamp: event.timeStamp,
type: event.type,
};
timeStamps.push(event.timeStamp);
eventLog.push(propertiesWeCareAbout);
};
const Component = () => {
const listener = usePress({
onPressStart: logEvent,
onPressEnd: logEvent,
onPressMove: logEvent,
onPress: logEvent,
});
return <button ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 10, y: 10, width: 100, height: 100});
target.pointerdown({
pointerType: 'mouse',
pageX: 15,
pageY: 16,
x: 30,
y: 31,
});
target.pointermove({
pointerType: 'mouse',
pageX: 16,
pageY: 17,
x: 31,
y: 32,
});
target.pointerup({
pointerType: 'mouse',
pageX: 17,
pageY: 18,
x: 32,
y: 33,
});
target.pointerdown({
pointerType: 'mouse',
pageX: 18,
pageY: 19,
x: 33,
y: 34,
});
expect(typeof timeStamps[0] === 'number').toBe(true);
expect(eventLog).toEqual([
{
pointerType: 'mouse',
pageX: 15,
pageY: 16,
screenX: 30,
screenY: 81,
clientX: 30,
clientY: 31,
target: ref.current,
timeStamp: timeStamps[0],
type: 'pressstart',
},
{
pointerType: 'mouse',
pageX: 16,
pageY: 17,
screenX: 31,
screenY: 82,
clientX: 31,
clientY: 32,
target: ref.current,
timeStamp: timeStamps[1],
type: 'pressmove',
},
{
pointerType: 'mouse',
pageX: 17,
pageY: 18,
screenX: 32,
screenY: 83,
clientX: 32,
clientY: 33,
target: ref.current,
timeStamp: timeStamps[2],
type: 'pressend',
},
{
pointerType: 'mouse',
pageX: 17,
pageY: 18,
screenX: 32,
screenY: 83,
clientX: 32,
clientY: 33,
target: ref.current,
timeStamp: timeStamps[3],
type: 'press',
},
{
pointerType: 'mouse',
pageX: 18,
pageY: 19,
screenX: 33,
screenY: 84,
clientX: 33,
clientY: 34,
target: ref.current,
timeStamp: timeStamps[4],
type: 'pressstart',
},
]);
});
if (hasPointerEvents) {
// TODO(T46067442): move these tests to core
/*
it('should properly only flush sync once when the event systems are mixed', () => {
const ref = React.createRef();
let renderCounts = 0;
function MyComponent() {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
ref={ref}
listeners={listener}
onClick={() => {
updateCounter(count => count + 1);
}}>
Press me
</button>
</div>
);
}
const newContainer = document.createElement('div');
const root = ReactDOM.unstable_createRoot(newContainer);
document.body.appendChild(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
const target = ref.current;
target.dispatchEvent(pointerdown({timeStamp: 100}));
target.dispatchEvent(pointerup({timeStamp: 100}));
target.dispatchEvent(click({timeStamp: 100}));
if (__DEV__) {
expect(renderCounts).toBe(2);
} else {
expect(renderCounts).toBe(1);
}
Scheduler.unstable_flushAll();
if (__DEV__) {
expect(renderCounts).toBe(4);
} else {
expect(renderCounts).toBe(2);
}
target.dispatchEvent(pointerdown({timeStamp: 100}));
target.dispatchEvent(pointerup({timeStamp: 100}));
// Ensure the timeStamp logic works
target.dispatchEvent(click({timeStamp: 101}));
if (__DEV__) {
expect(renderCounts).toBe(6);
} else {
expect(renderCounts).toBe(3);
}
Scheduler.unstable_flushAll();
document.body.removeChild(newContainer);
});
it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
const ref = React.createRef();
let renderCounts = 0;
function MyComponent() {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
ref={ref}
listeners={listener}
onClick={() => {
// This should flush synchronously
ReactDOM.unstable_flushDiscreteUpdates();
updateCounter(count => count + 1);
}}>
Press me
</button>
</div>
);
}
const newContainer = document.createElement('div');
const root = ReactDOM.unstable_createRoot(newContainer);
document.body.appendChild(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
const target = ref.current;
target.dispatchEvent(pointerdown({timeStamp: 100}));
target.dispatchEvent(pointerup({timeStamp: 100}));
target.dispatchEvent(click({timeStamp: 100}));
if (__DEV__) {
expect(renderCounts).toBe(4);
} else {
expect(renderCounts).toBe(2);
}
Scheduler.unstable_flushAll();
if (__DEV__) {
expect(renderCounts).toBe(6);
} else {
expect(renderCounts).toBe(3);
}
target.dispatchEvent(pointerdown({timeStamp: 100}));
target.dispatchEvent(pointerup({timeStamp: 100}));
// Ensure the timeStamp logic works
target.dispatchEvent(click({timeStamp: 101}));
if (__DEV__) {
expect(renderCounts).toBe(8);
} else {
expect(renderCounts).toBe(4);
}
Scheduler.unstable_flushAll();
document.body.removeChild(newContainer);
});
it(
'should only flush before outermost discrete event handler when mixing ' +
'event systems',
async () => {
const {useState} = React;
const button = React.createRef();
const ops = [];
function MyComponent() {
const [pressesCount, updatePressesCount] = useState(0);
const [clicksCount, updateClicksCount] = useState(0);
function handlePress() {
// This dispatches a synchronous, discrete event in the legacy event
// system. However, because it's nested inside the new event system,
// its updates should not flush until the end of the outer handler.
button.current.click();
// Text context should not have changed
ops.push(newContainer.textContent);
updatePressesCount(pressesCount + 1);
}
const listener = usePress({
onPress: handlePress,
});
return (
<div>
<button
listeners={listener}
ref={button}
onClick={() => updateClicksCount(clicksCount + 1)}>
Presses: {pressesCount}, Clicks: {clicksCount}
</button>
</div>
);
}
const newContainer = document.createElement('div');
document.body.appendChild(newContainer);
const root = ReactDOM.unstable_createRoot(newContainer);
root.render(<MyComponent />);
Scheduler.unstable_flushAll();
expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0');
const target = button.current;
target.dispatchEvent(pointerdown({timeStamp: 100}));
target.dispatchEvent(pointerup({timeStamp: 100}));
target.dispatchEvent(click({timeStamp: 100}));
Scheduler.unstable_flushAll();
expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1');
expect(ops).toEqual(['Presses: 0, Clicks: 0']);
},
);
*/
it('should work correctly with stopPropagation set to true', () => {
const ref = React.createRef();
const pointerDownEvent = jest.fn();
@@ -37,6 +37,9 @@ function createEvent(type, data = {}) {
if (data != null) {
Object.keys(data).forEach(key => {
const value = data[key];
if (key === 'timeStamp' && !value) {
return;
}
Object.defineProperty(event, key, {value});
});
}
@@ -83,6 +86,7 @@ function createPointerEvent(
tangentialPressure = 0,
tiltX = 0,
tiltY = 0,
timeStamp,
twist = 0,
width,
x = 0,
@@ -122,6 +126,7 @@ function createPointerEvent(
tangentialPressure,
tiltX,
tiltY,
timeStamp,
twist,
width: isMouse ? 1 : width != null ? width : defaultPointerSize,
});
@@ -171,6 +176,7 @@ function createMouseEvent(
screenX,
screenY,
shiftKey = false,
timeStamp,
x = 0,
y = 0,
} = {},
@@ -198,6 +204,7 @@ function createMouseEvent(
screenX: screenX === 0 ? screenX : x,
screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize,
shiftKey,
timeStamp,
});
}
@@ -209,6 +216,8 @@ function createTouchEvent(type, payload) {
let metaKey = false;
let preventDefault = emptyFunction;
let shiftKey = false;
let timeStamp;
if (firstTouch != null) {
if (firstTouch.altKey != null) {
altKey = firstTouch.altKey;
@@ -225,6 +234,9 @@ function createTouchEvent(type, payload) {
if (firstTouch.shiftKey != null) {
shiftKey = firstTouch.shiftKey;
}
if (firstTouch.timeStamp != null) {
timeStamp = firstTouch.timeStamp;
}
}
const touches = touchesPayload.map(
@@ -268,6 +280,7 @@ function createTouchEvent(type, payload) {
firesTouchEvents: true,
},
targetTouches: activeTouches,
timeStamp,
touches: activeTouches,
});
}