forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathSimpleEventPlugin-test.internal.js
495 lines (425 loc) · 14 KB
/
SimpleEventPlugin-test.internal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/**
* 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';
describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let ReactFeatureFlags;
let onClick;
let container;
function expectClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(1);
}
function expectNoClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(0);
}
function mounted(element) {
container = document.createElement('div');
document.body.appendChild(container);
element = ReactDOM.render(element, container);
return element;
}
beforeEach(function() {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
onClick = jest.fn();
});
afterEach(() => {
if (container && document.body.contains(container)) {
document.body.removeChild(container);
container = null;
}
});
it('A non-interactive tags click when disabled', function() {
const element = <div onClick={onClick} />;
expectClickThru(mounted(element));
});
it('A non-interactive tags clicks bubble when disabled', function() {
const element = mounted(
<div onClick={onClick}>
<div />
</div>,
);
const child = element.firstChild;
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('does not register a click when clicking a child of a disabled element', function() {
const element = mounted(
<button onClick={onClick} disabled={true}>
<span />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(0);
});
it('triggers click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClick={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers parent captured click events when target is a child of a disabled elements', function() {
const element = mounted(
<div onClickCapture={onClick}>
<button disabled={true}>
<span />
</button>
</div>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers captured click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClickCapture={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
describe(tagName, function() {
it('should forward clicks when it starts out not disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
});
expectClickThru(mounted(element));
});
it('should not forward clicks when it starts out disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
disabled: true,
});
expectNoClickThru(mounted(element));
});
it('should forward clicks when it becomes not disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
expectClickThru(element);
});
it('should not forward clicks when it becomes disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
expectNoClickThru(element);
});
it('should work correctly if the listener is changed', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: false}),
container,
);
expectClickThru(element);
});
});
});
it('batches updates that occur as a result of a nested event dispatch', () => {
container = document.createElement('div');
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {count: 0};
increment = () =>
this.setState(state => ({
count: state.count + 1,
}));
componentDidUpdate() {
ops.push(`didUpdate - Count: ${this.state.count}`);
}
render() {
return (
<button
ref={el => (button = el)}
onFocus={this.increment}
onClick={() => {
// The focus call synchronously dispatches a nested event. All of
// the updates in this handler should be batched together.
this.increment();
button.focus();
this.increment();
}}>
Count: {this.state.count}
</button>
);
}
}
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(ops).toEqual([]);
click();
// There should be exactly one update.
expect(ops).toEqual(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
describe('interactive events, in async mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
});
it('flushes pending interactive work before extracting event handler', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {disabled: false};
onClick = () => {
// Perform some side-effect
ops.push('Side-effect');
// Disable the button
this.setState({disabled: true});
};
render() {
ops.push(
`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(ops).toEqual([]);
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
expect(ops).toEqual(['render button: enabled']);
ops = [];
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button to trigger the side-effect
click();
expect(ops).toEqual([
// The handler fired
'Side-effect',
// but the component did not re-render yet, because it's async
]);
ops = [];
// Click the button again
click();
expect(ops).toEqual([
// 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
]);
ops = [];
// The handler should not fire again no matter how many times we
// click the handler.
click();
click();
click();
click();
click();
jest.runAllTimers();
expect(ops).toEqual([]);
});
it('end result of many interactive updates is deterministic', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
document.body.appendChild(container);
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>
);
}
}
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
expect(button.textContent).toEqual('Count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// The counter should not have updated yet because it's async
expect(button.textContent).toEqual('Count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work
jest.runAllTimers();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
it('flushes lowest pending interactive priority', () => {
container = document.createElement('div');
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {lowPriCount: 0};
render() {
return (
<button
ref={el => (button = el)}
onClick={
// Intentionally not using the updater form here
() => this.setState({lowPriCount: this.state.lowPriCount + 1})
}>
High-pri count: {this.props.highPriCount}, Low-pri count:{' '}
{this.state.lowPriCount}
</button>
);
}
}
class Wrapper extends React.Component {
state = {highPriCount: 0};
render() {
return (
<div
onClick={
// Intentionally not using the updater form here
() => this.setState({highPriCount: this.state.highPriCount + 1})
}>
<React.unstable_ConcurrentMode>
<Button highPriCount={this.state.highPriCount} />
</React.unstable_ConcurrentMode>
</div>
);
}
}
// Initial mount
ReactDOM.render(<Wrapper />, container);
expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// The high-pri counter should flush synchronously, but not the
// low-pri counter
expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work
jest.runAllTimers();
// Both counters should equal the total number of clicks
expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
});
});
describe('iOS bubbling click fix', function() {
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
it('does not add a local click to interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<button onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
it('adds a local click listener to non-interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<div onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
});
});