main
js 686 lines 16.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
11 import type Store from 'react-devtools-shared/src/devtools/store';
12
13 describe('editing interface', () => {
14 let PropTypes;
15 let React;
16 let ReactDOM;
17 let bridge: FrontendBridge;
18 let store: Store;
19
20 const act = (callback: Function) => {
21 callback();
22
23 jest.runAllTimers(); // Flush Bridge operations
24 };
25
26 const flushPendingUpdates = () => {
27 jest.runOnlyPendingTimers();
28 };
29
30 beforeEach(() => {
31 bridge = global.bridge;
32 store = global.store;
33 store.collapseNodesByDefault = false;
34
35 PropTypes = require('prop-types');
36
37 // Redirect all React/ReactDOM requires to the v15 UMD.
38 // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
39 jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
40 jest.mock('react-dom', () =>
41 jest.requireActual('react-dom-15/dist/react-dom.js'),
42 );
43
44 React = require('react');
45 ReactDOM = require('react-dom');
46 });
47
48 describe('props', () => {
49 let committedProps;
50 let id;
51
52 function mountTestApp() {
53 class ClassComponent extends React.Component {
54 componentDidMount() {
55 committedProps = this.props;
56 }
57 componentDidUpdate() {
58 committedProps = this.props;
59 }
60 render() {
61 return null;
62 }
63 }
64
65 act(() =>
66 ReactDOM.render(
67 React.createElement(ClassComponent, {
68 array: [1, 2, 3],
69 object: {nested: 'initial'},
70 shallow: 'initial',
71 }),
72 document.createElement('div'),
73 ),
74 );
75
76 id = ((store.getElementIDAtIndex(0): any): number);
77
78 expect(committedProps).toStrictEqual({
79 array: [1, 2, 3],
80 object: {
81 nested: 'initial',
82 },
83 shallow: 'initial',
84 });
85 }
86
87 // @reactVersion >= 16.0
88 it('should have editable values', () => {
89 mountTestApp();
90
91 function overrideProps(path, value) {
92 const rendererID = ((store.getRendererIDForElement(id): any): number);
93 bridge.send('overrideValueAtPath', {
94 id,
95 path,
96 rendererID,
97 type: 'props',
98 value,
99 });
100 flushPendingUpdates();
101 }
102
103 overrideProps(['shallow'], 'updated');
104 expect(committedProps).toStrictEqual({
105 array: [1, 2, 3],
106 object: {
107 nested: 'initial',
108 },
109 shallow: 'updated',
110 });
111 overrideProps(['object', 'nested'], 'updated');
112 expect(committedProps).toStrictEqual({
113 array: [1, 2, 3],
114 object: {
115 nested: 'updated',
116 },
117 shallow: 'updated',
118 });
119 overrideProps(['array', 1], 'updated');
120 expect(committedProps).toStrictEqual({
121 array: [1, 'updated', 3],
122 object: {
123 nested: 'updated',
124 },
125 shallow: 'updated',
126 });
127 });
128
129 // @reactVersion >= 16.0
130 it('should have editable paths', () => {
131 mountTestApp();
132
133 function renamePath(oldPath, newPath) {
134 const rendererID = ((store.getRendererIDForElement(id): any): number);
135 bridge.send('renamePath', {
136 id,
137 oldPath,
138 newPath,
139 rendererID,
140 type: 'props',
141 });
142 flushPendingUpdates();
143 }
144
145 renamePath(['shallow'], ['after']);
146 expect(committedProps).toStrictEqual({
147 array: [1, 2, 3],
148 object: {
149 nested: 'initial',
150 },
151 after: 'initial',
152 });
153 renamePath(['object', 'nested'], ['object', 'after']);
154 expect(committedProps).toStrictEqual({
155 array: [1, 2, 3],
156 object: {
157 after: 'initial',
158 },
159 after: 'initial',
160 });
161 });
162
163 // @reactVersion >= 16.0
164 it('should enable adding new object properties and array values', async () => {
165 await mountTestApp();
166
167 function overrideProps(path, value) {
168 const rendererID = ((store.getRendererIDForElement(id): any): number);
169 bridge.send('overrideValueAtPath', {
170 id,
171 path,
172 rendererID,
173 type: 'props',
174 value,
175 });
176 flushPendingUpdates();
177 }
178
179 overrideProps(['new'], 'value');
180 expect(committedProps).toStrictEqual({
181 array: [1, 2, 3],
182 object: {
183 nested: 'initial',
184 },
185 shallow: 'initial',
186 new: 'value',
187 });
188
189 overrideProps(['object', 'new'], 'value');
190 expect(committedProps).toStrictEqual({
191 array: [1, 2, 3],
192 object: {
193 nested: 'initial',
194 new: 'value',
195 },
196 shallow: 'initial',
197 new: 'value',
198 });
199
200 overrideProps(['array', 3], 'new value');
201 expect(committedProps).toStrictEqual({
202 array: [1, 2, 3, 'new value'],
203 object: {
204 nested: 'initial',
205 new: 'value',
206 },
207 shallow: 'initial',
208 new: 'value',
209 });
210 });
211
212 // @reactVersion >= 16.0
213 it('should have deletable keys', () => {
214 mountTestApp();
215
216 function deletePath(path) {
217 const rendererID = ((store.getRendererIDForElement(id): any): number);
218 bridge.send('deletePath', {
219 id,
220 path,
221 rendererID,
222 type: 'props',
223 });
224 flushPendingUpdates();
225 }
226
227 deletePath(['shallow']);
228 expect(committedProps).toStrictEqual({
229 array: [1, 2, 3],
230 object: {
231 nested: 'initial',
232 },
233 });
234 deletePath(['object', 'nested']);
235 expect(committedProps).toStrictEqual({
236 array: [1, 2, 3],
237 object: {},
238 });
239 deletePath(['array', 1]);
240 expect(committedProps).toStrictEqual({
241 array: [1, 3],
242 object: {},
243 });
244 });
245 });
246
247 describe('state', () => {
248 let committedState;
249 let id;
250
251 function mountTestApp() {
252 class ClassComponent extends React.Component {
253 state = {
254 array: [1, 2, 3],
255 object: {
256 nested: 'initial',
257 },
258 shallow: 'initial',
259 };
260 componentDidMount() {
261 committedState = this.state;
262 }
263 componentDidUpdate() {
264 committedState = this.state;
265 }
266 render() {
267 return null;
268 }
269 }
270
271 act(() =>
272 ReactDOM.render(
273 React.createElement(ClassComponent, {
274 object: {nested: 'initial'},
275 shallow: 'initial',
276 }),
277 document.createElement('div'),
278 ),
279 );
280
281 id = ((store.getElementIDAtIndex(0): any): number);
282
283 expect(committedState).toStrictEqual({
284 array: [1, 2, 3],
285 object: {
286 nested: 'initial',
287 },
288 shallow: 'initial',
289 });
290 }
291
292 // @reactVersion >= 16.0
293 it('should have editable values', () => {
294 mountTestApp();
295
296 function overrideState(path, value) {
297 const rendererID = ((store.getRendererIDForElement(id): any): number);
298 bridge.send('overrideValueAtPath', {
299 id,
300 path,
301 rendererID,
302 type: 'state',
303 value,
304 });
305 flushPendingUpdates();
306 }
307
308 overrideState(['shallow'], 'updated');
309 expect(committedState).toStrictEqual({
310 array: [1, 2, 3],
311 object: {nested: 'initial'},
312 shallow: 'updated',
313 });
314
315 overrideState(['object', 'nested'], 'updated');
316 expect(committedState).toStrictEqual({
317 array: [1, 2, 3],
318 object: {nested: 'updated'},
319 shallow: 'updated',
320 });
321
322 overrideState(['array', 1], 'updated');
323 expect(committedState).toStrictEqual({
324 array: [1, 'updated', 3],
325 object: {nested: 'updated'},
326 shallow: 'updated',
327 });
328 });
329
330 // @reactVersion >= 16.0
331 it('should have editable paths', () => {
332 mountTestApp();
333
334 function renamePath(oldPath, newPath) {
335 const rendererID = ((store.getRendererIDForElement(id): any): number);
336 bridge.send('renamePath', {
337 id,
338 oldPath,
339 newPath,
340 rendererID,
341 type: 'state',
342 });
343 flushPendingUpdates();
344 }
345
346 renamePath(['shallow'], ['after']);
347 expect(committedState).toStrictEqual({
348 array: [1, 2, 3],
349 object: {
350 nested: 'initial',
351 },
352 after: 'initial',
353 });
354
355 renamePath(['object', 'nested'], ['object', 'after']);
356 expect(committedState).toStrictEqual({
357 array: [1, 2, 3],
358 object: {
359 after: 'initial',
360 },
361 after: 'initial',
362 });
363 });
364
365 // @reactVersion >= 16.0
366 it('should enable adding new object properties and array values', async () => {
367 await mountTestApp();
368
369 function overrideState(path, value) {
370 const rendererID = ((store.getRendererIDForElement(id): any): number);
371 bridge.send('overrideValueAtPath', {
372 id,
373 path,
374 rendererID,
375 type: 'state',
376 value,
377 });
378 flushPendingUpdates();
379 }
380
381 overrideState(['new'], 'value');
382 expect(committedState).toStrictEqual({
383 array: [1, 2, 3],
384 object: {
385 nested: 'initial',
386 },
387 shallow: 'initial',
388 new: 'value',
389 });
390
391 overrideState(['object', 'new'], 'value');
392 expect(committedState).toStrictEqual({
393 array: [1, 2, 3],
394 object: {
395 nested: 'initial',
396 new: 'value',
397 },
398 shallow: 'initial',
399 new: 'value',
400 });
401
402 overrideState(['array', 3], 'new value');
403 expect(committedState).toStrictEqual({
404 array: [1, 2, 3, 'new value'],
405 object: {
406 nested: 'initial',
407 new: 'value',
408 },
409 shallow: 'initial',
410 new: 'value',
411 });
412 });
413
414 // @reactVersion >= 16.0
415 it('should have deletable keys', () => {
416 mountTestApp();
417
418 function deletePath(path) {
419 const rendererID = ((store.getRendererIDForElement(id): any): number);
420 bridge.send('deletePath', {
421 id,
422 path,
423 rendererID,
424 type: 'state',
425 });
426 flushPendingUpdates();
427 }
428
429 deletePath(['shallow']);
430 expect(committedState).toStrictEqual({
431 array: [1, 2, 3],
432 object: {
433 nested: 'initial',
434 },
435 });
436
437 deletePath(['object', 'nested']);
438 expect(committedState).toStrictEqual({
439 array: [1, 2, 3],
440 object: {},
441 });
442
443 deletePath(['array', 1]);
444 expect(committedState).toStrictEqual({
445 array: [1, 3],
446 object: {},
447 });
448 });
449 });
450
451 describe('context', () => {
452 let committedContext;
453 let id;
454
455 function mountTestApp() {
456 class LegacyContextProvider extends React.Component<any> {
457 static childContextTypes = {
458 array: PropTypes.array,
459 object: PropTypes.object,
460 shallow: PropTypes.string,
461 };
462 getChildContext() {
463 return {
464 array: [1, 2, 3],
465 object: {
466 nested: 'initial',
467 },
468 shallow: 'initial',
469 };
470 }
471 render() {
472 return this.props.children;
473 }
474 }
475
476 class ClassComponent extends React.Component<any> {
477 static contextTypes = {
478 array: PropTypes.array,
479 object: PropTypes.object,
480 shallow: PropTypes.string,
481 };
482 componentDidMount() {
483 committedContext = this.context;
484 }
485 componentDidUpdate() {
486 committedContext = this.context;
487 }
488 render() {
489 return null;
490 }
491 }
492
493 act(() =>
494 ReactDOM.render(
495 React.createElement(
496 LegacyContextProvider,
497 null,
498 React.createElement(ClassComponent),
499 ),
500 document.createElement('div'),
501 ),
502 );
503
504 // This test only covers Class components.
505 // Function components using legacy context are not editable.
506
507 id = ((store.getElementIDAtIndex(1): any): number);
508
509 expect(committedContext).toStrictEqual({
510 array: [1, 2, 3],
511 object: {
512 nested: 'initial',
513 },
514 shallow: 'initial',
515 });
516 }
517
518 // @reactVersion >= 16.0
519 it('should have editable values', () => {
520 mountTestApp();
521
522 function overrideContext(path, value) {
523 const rendererID = ((store.getRendererIDForElement(id): any): number);
524
525 bridge.send('overrideValueAtPath', {
526 id,
527 path,
528 rendererID,
529 type: 'context',
530 value,
531 });
532 flushPendingUpdates();
533 }
534
535 overrideContext(['shallow'], 'updated');
536 expect(committedContext).toStrictEqual({
537 array: [1, 2, 3],
538 object: {
539 nested: 'initial',
540 },
541 shallow: 'updated',
542 });
543
544 overrideContext(['object', 'nested'], 'updated');
545 expect(committedContext).toStrictEqual({
546 array: [1, 2, 3],
547 object: {
548 nested: 'updated',
549 },
550 shallow: 'updated',
551 });
552
553 overrideContext(['array', 1], 'updated');
554 expect(committedContext).toStrictEqual({
555 array: [1, 'updated', 3],
556 object: {
557 nested: 'updated',
558 },
559 shallow: 'updated',
560 });
561 });
562
563 // @reactVersion >= 16.0
564 it('should have editable paths', () => {
565 mountTestApp();
566
567 function renamePath(oldPath, newPath) {
568 const rendererID = ((store.getRendererIDForElement(id): any): number);
569
570 bridge.send('renamePath', {
571 id,
572 oldPath,
573 newPath,
574 rendererID,
575 type: 'context',
576 });
577 flushPendingUpdates();
578 }
579
580 renamePath(['shallow'], ['after']);
581 expect(committedContext).toStrictEqual({
582 array: [1, 2, 3],
583 object: {
584 nested: 'initial',
585 },
586 after: 'initial',
587 });
588
589 renamePath(['object', 'nested'], ['object', 'after']);
590 expect(committedContext).toStrictEqual({
591 array: [1, 2, 3],
592 object: {
593 after: 'initial',
594 },
595 after: 'initial',
596 });
597 });
598
599 // @reactVersion >= 16.0
600 it('should enable adding new object properties and array values', async () => {
601 await mountTestApp();
602
603 function overrideContext(path, value) {
604 const rendererID = ((store.getRendererIDForElement(id): any): number);
605
606 bridge.send('overrideValueAtPath', {
607 id,
608 path,
609 rendererID,
610 type: 'context',
611 value,
612 });
613 flushPendingUpdates();
614 }
615
616 overrideContext(['new'], 'value');
617 expect(committedContext).toStrictEqual({
618 array: [1, 2, 3],
619 object: {
620 nested: 'initial',
621 },
622 shallow: 'initial',
623 new: 'value',
624 });
625
626 overrideContext(['object', 'new'], 'value');
627 expect(committedContext).toStrictEqual({
628 array: [1, 2, 3],
629 object: {
630 nested: 'initial',
631 new: 'value',
632 },
633 shallow: 'initial',
634 new: 'value',
635 });
636
637 overrideContext(['array', 3], 'new value');
638 expect(committedContext).toStrictEqual({
639 array: [1, 2, 3, 'new value'],
640 object: {
641 nested: 'initial',
642 new: 'value',
643 },
644 shallow: 'initial',
645 new: 'value',
646 });
647 });
648
649 // @reactVersion >= 16.0
650 it('should have deletable keys', () => {
651 mountTestApp();
652
653 function deletePath(path) {
654 const rendererID = ((store.getRendererIDForElement(id): any): number);
655
656 bridge.send('deletePath', {
657 id,
658 path,
659 rendererID,
660 type: 'context',
661 });
662 flushPendingUpdates();
663 }
664
665 deletePath(['shallow']);
666 expect(committedContext).toStrictEqual({
667 array: [1, 2, 3],
668 object: {
669 nested: 'initial',
670 },
671 });
672
673 deletePath(['object', 'nested']);
674 expect(committedContext).toStrictEqual({
675 array: [1, 2, 3],
676 object: {},
677 });
678
679 deletePath(['array', 1]);
680 expect(committedContext).toStrictEqual({
681 array: [1, 3],
682 object: {},
683 });
684 });
685 });
686 });