1
+/**
2
+ * Copyright (c) Facebook, Inc. and its 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
+ let utils;
20
+
21
+ const flushPendingUpdates = () => {
22
+ jest.runOnlyPendingTimers();
23
+ };
24
+
25
+ beforeEach(() => {
26
+ utils = require('./utils');
27
+
28
+ bridge = global.bridge;
29
+ store = global.store;
30
+ store.collapseNodesByDefault = false;
31
+
32
+ PropTypes = require('prop-types');
33
+ React = require('react');
34
+ ReactDOM = require('react-dom');
35
+ });
36
+
37
+ describe('props', () => {
38
+ let committedClassProps;
39
+ let committedFunctionProps;
40
+ let classID;
41
+ let functionID;
42
+
43
+ async function mountTestApp() {
44
+ class ClassComponent extends React.Component {
45
+ componentDidMount() {
46
+ committedClassProps = this.props;
47
+ }
48
+ componentDidUpdate() {
49
+ committedClassProps = this.props;
50
+ }
51
+ render() {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function FunctionComponent(props) {
57
+ React.useLayoutEffect(() => {
58
+ committedFunctionProps = props;
59
+ });
60
+ return null;
61
+ }
62
+
63
+ const container = document.createElement('div');
64
+ await utils.actAsync(() =>
65
+ ReactDOM.render(
66
+ <>
67
+ <ClassComponent
68
+ array={[1, 2, 3]}
69
+ object={{nested: 'initial'}}
70
+ shallow="initial"
71
+ />
72
+ ,
73
+ <FunctionComponent
74
+ array={[1, 2, 3]}
75
+ object={{nested: 'initial'}}
76
+ shallow="initial"
77
+ />
78
+ ,
79
+ </>,
80
+ container,
81
+ ),
82
+ );
83
+
84
+ classID = ((store.getElementIDAtIndex(0): any): number);
85
+ functionID = ((store.getElementIDAtIndex(1): any): number);
86
+
87
+ expect(committedClassProps).toStrictEqual({
88
+ array: [1, 2, 3],
89
+ object: {
90
+ nested: 'initial',
91
+ },
92
+ shallow: 'initial',
93
+ });
94
+ expect(committedFunctionProps).toStrictEqual({
95
+ array: [1, 2, 3],
96
+ object: {
97
+ nested: 'initial',
98
+ },
99
+ shallow: 'initial',
100
+ });
101
+ }
102
+
103
+ it('should have editable values', async () => {
104
+ await mountTestApp();
105
+
106
+ function overrideProps(id, path, value) {
107
+ const rendererID = utils.getRendererID();
108
+ bridge.send('overrideValueAtPath', {
109
+ id,
110
+ path,
111
+ rendererID,
112
+ type: 'props',
113
+ value,
114
+ });
115
+ flushPendingUpdates();
116
+ }
117
+
118
+ overrideProps(classID, ['shallow'], 'updated');
119
+ expect(committedClassProps).toStrictEqual({
120
+ array: [1, 2, 3],
121
+ object: {
122
+ nested: 'initial',
123
+ },
124
+ shallow: 'updated',
125
+ });
126
+ overrideProps(classID, ['object', 'nested'], 'updated');
127
+ expect(committedClassProps).toStrictEqual({
128
+ array: [1, 2, 3],
129
+ object: {
130
+ nested: 'updated',
131
+ },
132
+ shallow: 'updated',
133
+ });
134
+ overrideProps(classID, ['array', 1], 'updated');
135
+ expect(committedClassProps).toStrictEqual({
136
+ array: [1, 'updated', 3],
137
+ object: {
138
+ nested: 'updated',
139
+ },
140
+ shallow: 'updated',
141
+ });
142
+
143
+ overrideProps(functionID, ['shallow'], 'updated');
144
+ expect(committedFunctionProps).toStrictEqual({
145
+ array: [1, 2, 3],
146
+ object: {
147
+ nested: 'initial',
148
+ },
149
+ shallow: 'updated',
150
+ });
151
+ overrideProps(functionID, ['object', 'nested'], 'updated');
152
+ expect(committedFunctionProps).toStrictEqual({
153
+ array: [1, 2, 3],
154
+ object: {
155
+ nested: 'updated',
156
+ },
157
+ shallow: 'updated',
158
+ });
159
+ overrideProps(functionID, ['array', 1], 'updated');
160
+ expect(committedFunctionProps).toStrictEqual({
161
+ array: [1, 'updated', 3],
162
+ object: {
163
+ nested: 'updated',
164
+ },
165
+ shallow: 'updated',
166
+ });
167
+ });
168
+
169
+ // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
170
+ it('should still support overriding prop values with legacy backend methods', async () => {
171
+ await mountTestApp();
172
+
173
+ function overrideProps(id, path, value) {
174
+ const rendererID = utils.getRendererID();
175
+ bridge.send('overrideProps', {
176
+ id,
177
+ path,
178
+ rendererID,
179
+ value,
180
+ });
181
+ flushPendingUpdates();
182
+ }
183
+
184
+ overrideProps(classID, ['object', 'nested'], 'updated');
185
+ expect(committedClassProps).toStrictEqual({
186
+ array: [1, 2, 3],
187
+ object: {
188
+ nested: 'updated',
189
+ },
190
+ shallow: 'initial',
191
+ });
192
+
193
+ overrideProps(functionID, ['shallow'], 'updated');
194
+ expect(committedFunctionProps).toStrictEqual({
195
+ array: [1, 2, 3],
196
+ object: {
197
+ nested: 'initial',
198
+ },
199
+ shallow: 'updated',
200
+ });
201
+ });
202
+
203
+ it('should have editable paths', async () => {
204
+ await mountTestApp();
205
+
206
+ function renamePath(id, oldPath, newPath) {
207
+ const rendererID = utils.getRendererID();
208
+ bridge.send('renamePath', {
209
+ id,
210
+ oldPath,
211
+ newPath,
212
+ rendererID,
213
+ type: 'props',
214
+ });
215
+ flushPendingUpdates();
216
+ }
217
+
218
+ renamePath(classID, ['shallow'], ['after']);
219
+ expect(committedClassProps).toStrictEqual({
220
+ array: [1, 2, 3],
221
+ object: {
222
+ nested: 'initial',
223
+ },
224
+ after: 'initial',
225
+ });
226
+ renamePath(classID, ['object', 'nested'], ['object', 'after']);
227
+ expect(committedClassProps).toStrictEqual({
228
+ array: [1, 2, 3],
229
+ object: {
230
+ after: 'initial',
231
+ },
232
+ after: 'initial',
233
+ });
234
+
235
+ renamePath(functionID, ['shallow'], ['after']);
236
+ expect(committedFunctionProps).toStrictEqual({
237
+ array: [1, 2, 3],
238
+ object: {
239
+ nested: 'initial',
240
+ },
241
+ after: 'initial',
242
+ });
243
+ renamePath(functionID, ['object', 'nested'], ['object', 'after']);
244
+ expect(committedFunctionProps).toStrictEqual({
245
+ array: [1, 2, 3],
246
+ object: {
247
+ after: 'initial',
248
+ },
249
+ after: 'initial',
250
+ });
251
+ });
252
+
253
+ it('should enable adding new object properties and array values', async () => {
254
+ await mountTestApp();
255
+
256
+ function overrideProps(id, path, value) {
257
+ const rendererID = utils.getRendererID();
258
+ bridge.send('overrideValueAtPath', {
259
+ id,
260
+ path,
261
+ rendererID,
262
+ type: 'props',
263
+ value,
264
+ });
265
+ flushPendingUpdates();
266
+ }
267
+
268
+ overrideProps(classID, ['new'], 'value');
269
+ expect(committedClassProps).toStrictEqual({
270
+ array: [1, 2, 3],
271
+ object: {
272
+ nested: 'initial',
273
+ },
274
+ shallow: 'initial',
275
+ new: 'value',
276
+ });
277
+
278
+ overrideProps(classID, ['object', 'new'], 'value');
279
+ expect(committedClassProps).toStrictEqual({
280
+ array: [1, 2, 3],
281
+ object: {
282
+ nested: 'initial',
283
+ new: 'value',
284
+ },
285
+ shallow: 'initial',
286
+ new: 'value',
287
+ });
288
+
289
+ overrideProps(classID, ['array', 3], 'new value');
290
+ expect(committedClassProps).toStrictEqual({
291
+ array: [1, 2, 3, 'new value'],
292
+ object: {
293
+ nested: 'initial',
294
+ new: 'value',
295
+ },
296
+ shallow: 'initial',
297
+ new: 'value',
298
+ });
299
+
300
+ overrideProps(functionID, ['new'], 'value');
301
+ expect(committedFunctionProps).toStrictEqual({
302
+ array: [1, 2, 3],
303
+ object: {
304
+ nested: 'initial',
305
+ },
306
+ shallow: 'initial',
307
+ new: 'value',
308
+ });
309
+
310
+ overrideProps(functionID, ['object', 'new'], 'value');
311
+ expect(committedFunctionProps).toStrictEqual({
312
+ array: [1, 2, 3],
313
+ object: {
314
+ nested: 'initial',
315
+ new: 'value',
316
+ },
317
+ shallow: 'initial',
318
+ new: 'value',
319
+ });
320
+
321
+ overrideProps(functionID, ['array', 3], 'new value');
322
+ expect(committedFunctionProps).toStrictEqual({
323
+ array: [1, 2, 3, 'new value'],
324
+ object: {
325
+ nested: 'initial',
326
+ new: 'value',
327
+ },
328
+ shallow: 'initial',
329
+ new: 'value',
330
+ });
331
+ });
332
+
333
+ it('should have deletable keys', async () => {
334
+ await mountTestApp();
335
+
336
+ function deletePath(id, path) {
337
+ const rendererID = utils.getRendererID();
338
+ bridge.send('deletePath', {
339
+ id,
340
+ path,
341
+ rendererID,
342
+ type: 'props',
343
+ });
344
+ flushPendingUpdates();
345
+ }
346
+
347
+ deletePath(classID, ['shallow']);
348
+ expect(committedClassProps).toStrictEqual({
349
+ array: [1, 2, 3],
350
+ object: {
351
+ nested: 'initial',
352
+ },
353
+ });
354
+ deletePath(classID, ['object', 'nested']);
355
+ expect(committedClassProps).toStrictEqual({
356
+ array: [1, 2, 3],
357
+ object: {},
358
+ });
359
+ deletePath(classID, ['array', 1]);
360
+ expect(committedClassProps).toStrictEqual({
361
+ array: [1, 3],
362
+ object: {},
363
+ });
364
+
365
+ deletePath(functionID, ['shallow']);
366
+ expect(committedFunctionProps).toStrictEqual({
367
+ array: [1, 2, 3],
368
+ object: {
369
+ nested: 'initial',
370
+ },
371
+ });
372
+ deletePath(functionID, ['object', 'nested']);
373
+ expect(committedFunctionProps).toStrictEqual({
374
+ array: [1, 2, 3],
375
+ object: {},
376
+ });
377
+ deletePath(functionID, ['array', 1]);
378
+ expect(committedFunctionProps).toStrictEqual({
379
+ array: [1, 3],
380
+ object: {},
381
+ });
382
+ });
383
+ });
384
+
385
+ describe('state', () => {
386
+ let committedState;
387
+ let id;
388
+
389
+ async function mountTestApp() {
390
+ class ClassComponent extends React.Component {
391
+ state = {
392
+ array: [1, 2, 3],
393
+ object: {
394
+ nested: 'initial',
395
+ },
396
+ shallow: 'initial',
397
+ };
398
+ componentDidMount() {
399
+ committedState = this.state;
400
+ }
401
+ componentDidUpdate() {
402
+ committedState = this.state;
403
+ }
404
+ render() {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ const container = document.createElement('div');
410
+ await utils.actAsync(() =>
411
+ ReactDOM.render(
412
+ <ClassComponent object={{nested: 'initial'}} shallow="initial" />,
413
+ container,
414
+ ),
415
+ );
416
+
417
+ id = ((store.getElementIDAtIndex(0): any): number);
418
+
419
+ expect(committedState).toStrictEqual({
420
+ array: [1, 2, 3],
421
+ object: {
422
+ nested: 'initial',
423
+ },
424
+ shallow: 'initial',
425
+ });
426
+ }
427
+
428
+ it('should have editable values', async () => {
429
+ await mountTestApp();
430
+
431
+ function overrideState(path, value) {
432
+ const rendererID = utils.getRendererID();
433
+ bridge.send('overrideValueAtPath', {
434
+ id,
435
+ path,
436
+ rendererID,
437
+ type: 'state',
438
+ value,
439
+ });
440
+ flushPendingUpdates();
441
+ }
442
+
443
+ overrideState(['shallow'], 'updated');
444
+ expect(committedState).toStrictEqual({
445
+ array: [1, 2, 3],
446
+ object: {nested: 'initial'},
447
+ shallow: 'updated',
448
+ });
449
+
450
+ overrideState(['object', 'nested'], 'updated');
451
+ expect(committedState).toStrictEqual({
452
+ array: [1, 2, 3],
453
+ object: {nested: 'updated'},
454
+ shallow: 'updated',
455
+ });
456
+
457
+ overrideState(['array', 1], 'updated');
458
+ expect(committedState).toStrictEqual({
459
+ array: [1, 'updated', 3],
460
+ object: {nested: 'updated'},
461
+ shallow: 'updated',
462
+ });
463
+ });
464
+
465
+ // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
466
+ it('should still support overriding state values with legacy backend methods', async () => {
467
+ await mountTestApp();
468
+
469
+ function overrideState(path, value) {
470
+ const rendererID = utils.getRendererID();
471
+ bridge.send('overrideState', {
472
+ id,
473
+ path,
474
+ rendererID,
475
+ value,
476
+ });
477
+ flushPendingUpdates();
478
+ }
479
+
480
+ overrideState(['array', 1], 'updated');
481
+ expect(committedState).toStrictEqual({
482
+ array: [1, 'updated', 3],
483
+ object: {nested: 'initial'},
484
+ shallow: 'initial',
485
+ });
486
+ });
487
+
488
+ it('should have editable paths', async () => {
489
+ await mountTestApp();
490
+
491
+ function renamePath(oldPath, newPath) {
492
+ const rendererID = utils.getRendererID();
493
+ bridge.send('renamePath', {
494
+ id,
495
+ oldPath,
496
+ newPath,
497
+ rendererID,
498
+ type: 'state',
499
+ });
500
+ flushPendingUpdates();
501
+ }
502
+
503
+ renamePath(['shallow'], ['after']);
504
+ expect(committedState).toStrictEqual({
505
+ array: [1, 2, 3],
506
+ object: {
507
+ nested: 'initial',
508
+ },
509
+ after: 'initial',
510
+ });
511
+
512
+ renamePath(['object', 'nested'], ['object', 'after']);
513
+ expect(committedState).toStrictEqual({
514
+ array: [1, 2, 3],
515
+ object: {
516
+ after: 'initial',
517
+ },
518
+ after: 'initial',
519
+ });
520
+ });
521
+
522
+ it('should enable adding new object properties and array values', async () => {
523
+ await mountTestApp();
524
+
525
+ function overrideState(path, value) {
526
+ const rendererID = utils.getRendererID();
527
+ bridge.send('overrideValueAtPath', {
528
+ id,
529
+ path,
530
+ rendererID,
531
+ type: 'state',
532
+ value,
533
+ });
534
+ flushPendingUpdates();
535
+ }
536
+
537
+ overrideState(['new'], 'value');
538
+ expect(committedState).toStrictEqual({
539
+ array: [1, 2, 3],
540
+ object: {
541
+ nested: 'initial',
542
+ },
543
+ shallow: 'initial',
544
+ new: 'value',
545
+ });
546
+
547
+ overrideState(['object', 'new'], 'value');
548
+ expect(committedState).toStrictEqual({
549
+ array: [1, 2, 3],
550
+ object: {
551
+ nested: 'initial',
552
+ new: 'value',
553
+ },
554
+ shallow: 'initial',
555
+ new: 'value',
556
+ });
557
+
558
+ overrideState(['array', 3], 'new value');
559
+ expect(committedState).toStrictEqual({
560
+ array: [1, 2, 3, 'new value'],
561
+ object: {
562
+ nested: 'initial',
563
+ new: 'value',
564
+ },
565
+ shallow: 'initial',
566
+ new: 'value',
567
+ });
568
+ });
569
+
570
+ it('should have deletable keys', async () => {
571
+ await mountTestApp();
572
+
573
+ function deletePath(path) {
574
+ const rendererID = utils.getRendererID();
575
+ bridge.send('deletePath', {
576
+ id,
577
+ path,
578
+ rendererID,
579
+ type: 'state',
580
+ });
581
+ flushPendingUpdates();
582
+ }
583
+
584
+ deletePath(['shallow']);
585
+ expect(committedState).toStrictEqual({
586
+ array: [1, 2, 3],
587
+ object: {
588
+ nested: 'initial',
589
+ },
590
+ });
591
+
592
+ deletePath(['object', 'nested']);
593
+ expect(committedState).toStrictEqual({
594
+ array: [1, 2, 3],
595
+ object: {},
596
+ });
597
+
598
+ deletePath(['array', 1]);
599
+ expect(committedState).toStrictEqual({
600
+ array: [1, 3],
601
+ object: {},
602
+ });
603
+ });
604
+ });
605
+
606
+ describe('hooks', () => {
607
+ let committedState;
608
+ let hookID;
609
+ let id;
610
+
611
+ async function mountTestApp() {
612
+ function FunctionComponent() {
613
+ const [state] = React.useState({
614
+ array: [1, 2, 3],
615
+ object: {
616
+ nested: 'initial',
617
+ },
618
+ shallow: 'initial',
619
+ });
620
+ React.useLayoutEffect(() => {
621
+ committedState = state;
622
+ });
623
+ return null;
624
+ }
625
+
626
+ const container = document.createElement('div');
627
+ await utils.actAsync(() =>
628
+ ReactDOM.render(<FunctionComponent />, container),
629
+ );
630
+
631
+ hookID = 0; // index
632
+ id = ((store.getElementIDAtIndex(0): any): number);
633
+
634
+ expect(committedState).toStrictEqual({
635
+ array: [1, 2, 3],
636
+ object: {
637
+ nested: 'initial',
638
+ },
639
+ shallow: 'initial',
640
+ });
641
+ }
642
+
643
+ it('should have editable values', async () => {
644
+ await mountTestApp();
645
+
646
+ function overrideHookState(path, value) {
647
+ const rendererID = utils.getRendererID();
648
+ bridge.send('overrideValueAtPath', {
649
+ hookID,
650
+ id,
651
+ path,
652
+ rendererID,
653
+ type: 'hooks',
654
+ value,
655
+ });
656
+ flushPendingUpdates();
657
+ }
658
+
659
+ overrideHookState(['shallow'], 'updated');
660
+ expect(committedState).toStrictEqual({
661
+ array: [1, 2, 3],
662
+ object: {
663
+ nested: 'initial',
664
+ },
665
+ shallow: 'updated',
666
+ });
667
+
668
+ overrideHookState(['object', 'nested'], 'updated');
669
+ expect(committedState).toStrictEqual({
670
+ array: [1, 2, 3],
671
+ object: {
672
+ nested: 'updated',
673
+ },
674
+ shallow: 'updated',
675
+ });
676
+
677
+ overrideHookState(['array', 1], 'updated');
678
+ expect(committedState).toStrictEqual({
679
+ array: [1, 'updated', 3],
680
+ object: {
681
+ nested: 'updated',
682
+ },
683
+ shallow: 'updated',
684
+ });
685
+ });
686
+
687
+ // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
688
+ it('should still support overriding hook values with legacy backend methods', async () => {
689
+ await mountTestApp();
690
+
691
+ function overrideHookState(path, value) {
692
+ const rendererID = utils.getRendererID();
693
+ bridge.send('overrideHookState', {
694
+ hookID,
695
+ id,
696
+ path,
697
+ rendererID,
698
+ value,
699
+ });
700
+ flushPendingUpdates();
701
+ }
702
+
703
+ overrideHookState(['shallow'], 'updated');
704
+ expect(committedState).toStrictEqual({
705
+ array: [1, 2, 3],
706
+ object: {
707
+ nested: 'initial',
708
+ },
709
+ shallow: 'updated',
710
+ });
711
+ });
712
+
713
+ it('should have editable paths', async () => {
714
+ await mountTestApp();
715
+
716
+ function renamePath(oldPath, newPath) {
717
+ const rendererID = utils.getRendererID();
718
+ bridge.send('renamePath', {
719
+ id,
720
+ hookID,
721
+ oldPath,
722
+ newPath,
723
+ rendererID,
724
+ type: 'hooks',
725
+ });
726
+ flushPendingUpdates();
727
+ }
728
+
729
+ renamePath(['shallow'], ['after']);
730
+ expect(committedState).toStrictEqual({
731
+ array: [1, 2, 3],
732
+ object: {
733
+ nested: 'initial',
734
+ },
735
+ after: 'initial',
736
+ });
737
+
738
+ renamePath(['object', 'nested'], ['object', 'after']);
739
+ expect(committedState).toStrictEqual({
740
+ array: [1, 2, 3],
741
+ object: {
742
+ after: 'initial',
743
+ },
744
+ after: 'initial',
745
+ });
746
+ });
747
+
748
+ it('should enable adding new object properties and array values', async () => {
749
+ await mountTestApp();
750
+
751
+ function overrideHookState(path, value) {
752
+ const rendererID = utils.getRendererID();
753
+ bridge.send('overrideValueAtPath', {
754
+ hookID,
755
+ id,
756
+ path,
757
+ rendererID,
758
+ type: 'hooks',
759
+ value,
760
+ });
761
+ flushPendingUpdates();
762
+ }
763
+
764
+ overrideHookState(['new'], 'value');
765
+ expect(committedState).toStrictEqual({
766
+ array: [1, 2, 3],
767
+ object: {
768
+ nested: 'initial',
769
+ },
770
+ shallow: 'initial',
771
+ new: 'value',
772
+ });
773
+
774
+ overrideHookState(['object', 'new'], 'value');
775
+ expect(committedState).toStrictEqual({
776
+ array: [1, 2, 3],
777
+ object: {
778
+ nested: 'initial',
779
+ new: 'value',
780
+ },
781
+ shallow: 'initial',
782
+ new: 'value',
783
+ });
784
+
785
+ overrideHookState(['array', 3], 'new value');
786
+ expect(committedState).toStrictEqual({
787
+ array: [1, 2, 3, 'new value'],
788
+ object: {
789
+ nested: 'initial',
790
+ new: 'value',
791
+ },
792
+ shallow: 'initial',
793
+ new: 'value',
794
+ });
795
+ });
796
+
797
+ it('should have deletable keys', async () => {
798
+ await mountTestApp();
799
+
800
+ function deletePath(path) {
801
+ const rendererID = utils.getRendererID();
802
+ bridge.send('deletePath', {
803
+ hookID,
804
+ id,
805
+ path,
806
+ rendererID,
807
+ type: 'hooks',
808
+ });
809
+ flushPendingUpdates();
810
+ }
811
+
812
+ deletePath(['shallow']);
813
+ expect(committedState).toStrictEqual({
814
+ array: [1, 2, 3],
815
+ object: {
816
+ nested: 'initial',
817
+ },
818
+ });
819
+
820
+ deletePath(['object', 'nested']);
821
+ expect(committedState).toStrictEqual({
822
+ array: [1, 2, 3],
823
+ object: {},
824
+ });
825
+
826
+ deletePath(['array', 1]);
827
+ expect(committedState).toStrictEqual({
828
+ array: [1, 3],
829
+ object: {},
830
+ });
831
+ });
832
+ });
833
+
834
+ describe('context', () => {
835
+ let committedContext;
836
+ let id;
837
+
838
+ async function mountTestApp() {
839
+ class LegacyContextProvider extends React.Component<any> {
840
+ static childContextTypes = {
841
+ array: PropTypes.array,
842
+ object: PropTypes.object,
843
+ shallow: PropTypes.string,
844
+ };
845
+ getChildContext() {
846
+ return {
847
+ array: [1, 2, 3],
848
+ object: {
849
+ nested: 'initial',
850
+ },
851
+ shallow: 'initial',
852
+ };
853
+ }
854
+ render() {
855
+ return this.props.children;
856
+ }
857
+ }
858
+
859
+ class ClassComponent extends React.Component<any> {
860
+ static contextTypes = {
861
+ array: PropTypes.array,
862
+ object: PropTypes.object,
863
+ shallow: PropTypes.string,
864
+ };
865
+ componentDidMount() {
866
+ committedContext = this.context;
867
+ }
868
+ componentDidUpdate() {
869
+ committedContext = this.context;
870
+ }
871
+ render() {
872
+ return null;
873
+ }
874
+ }
875
+
876
+ const container = document.createElement('div');
877
+ await utils.actAsync(() =>
878
+ ReactDOM.render(
879
+ <LegacyContextProvider>
880
+ <ClassComponent />
881
+ </LegacyContextProvider>,
882
+ container,
883
+ ),
884
+ );
885
+
886
+ // This test only covers Class components.
887
+ // Function components using legacy context are not editable.
888
+
889
+ id = ((store.getElementIDAtIndex(1): any): number);
890
+
891
+ expect(committedContext).toStrictEqual({
892
+ array: [1, 2, 3],
893
+ object: {
894
+ nested: 'initial',
895
+ },
896
+ shallow: 'initial',
897
+ });
898
+ }
899
+
900
+ it('should have editable values', async () => {
901
+ await mountTestApp();
902
+
903
+ function overrideContext(path, value) {
904
+ const rendererID = utils.getRendererID();
905
+
906
+ // To simplify hydration and display of primitive context values (e.g. number, string)
907
+ // the inspectElement() method wraps context in a {value: ...} object.
908
+ path = ['value', ...path];
909
+
910
+ bridge.send('overrideValueAtPath', {
911
+ id,
912
+ path,
913
+ rendererID,
914
+ type: 'context',
915
+ value,
916
+ });
917
+ flushPendingUpdates();
918
+ }
919
+
920
+ overrideContext(['shallow'], 'updated');
921
+ expect(committedContext).toStrictEqual({
922
+ array: [1, 2, 3],
923
+ object: {
924
+ nested: 'initial',
925
+ },
926
+ shallow: 'updated',
927
+ });
928
+
929
+ overrideContext(['object', 'nested'], 'updated');
930
+ expect(committedContext).toStrictEqual({
931
+ array: [1, 2, 3],
932
+ object: {
933
+ nested: 'updated',
934
+ },
935
+ shallow: 'updated',
936
+ });
937
+
938
+ overrideContext(['array', 1], 'updated');
939
+ expect(committedContext).toStrictEqual({
940
+ array: [1, 'updated', 3],
941
+ object: {
942
+ nested: 'updated',
943
+ },
944
+ shallow: 'updated',
945
+ });
946
+ });
947
+
948
+ // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
949
+ it('should still support overriding context values with legacy backend methods', async () => {
950
+ await mountTestApp();
951
+
952
+ function overrideContext(path, value) {
953
+ const rendererID = utils.getRendererID();
954
+
955
+ // To simplify hydration and display of primitive context values (e.g. number, string)
956
+ // the inspectElement() method wraps context in a {value: ...} object.
957
+ path = ['value', ...path];
958
+
959
+ bridge.send('overrideContext', {
960
+ id,
961
+ path,
962
+ rendererID,
963
+ value,
964
+ });
965
+ flushPendingUpdates();
966
+ }
967
+
968
+ overrideContext(['object', 'nested'], 'updated');
969
+ expect(committedContext).toStrictEqual({
970
+ array: [1, 2, 3],
971
+ object: {
972
+ nested: 'updated',
973
+ },
974
+ shallow: 'initial',
975
+ });
976
+ });
977
+
978
+ it('should have editable paths', async () => {
979
+ await mountTestApp();
980
+
981
+ function renamePath(oldPath, newPath) {
982
+ const rendererID = utils.getRendererID();
983
+
984
+ // To simplify hydration and display of primitive context values (e.g. number, string)
985
+ // the inspectElement() method wraps context in a {value: ...} object.
986
+ oldPath = ['value', ...oldPath];
987
+ newPath = ['value', ...newPath];
988
+
989
+ bridge.send('renamePath', {
990
+ id,
991
+ oldPath,
992
+ newPath,
993
+ rendererID,
994
+ type: 'context',
995
+ });
996
+ flushPendingUpdates();
997
+ }
998
+
999
+ renamePath(['shallow'], ['after']);
1000
+ expect(committedContext).toStrictEqual({
1001
+ array: [1, 2, 3],
1002
+ object: {
1003
+ nested: 'initial',
1004
+ },
1005
+ after: 'initial',
1006
+ });
1007
+
1008
+ renamePath(['object', 'nested'], ['object', 'after']);
1009
+ expect(committedContext).toStrictEqual({
1010
+ array: [1, 2, 3],
1011
+ object: {
1012
+ after: 'initial',
1013
+ },
1014
+ after: 'initial',
1015
+ });
1016
+ });
1017
+
1018
+ it('should enable adding new object properties and array values', async () => {
1019
+ await mountTestApp();
1020
+
1021
+ function overrideContext(path, value) {
1022
+ const rendererID = utils.getRendererID();
1023
+
1024
+ // To simplify hydration and display of primitive context values (e.g. number, string)
1025
+ // the inspectElement() method wraps context in a {value: ...} object.
1026
+ path = ['value', ...path];
1027
+
1028
+ bridge.send('overrideValueAtPath', {
1029
+ id,
1030
+ path,
1031
+ rendererID,
1032
+ type: 'context',
1033
+ value,
1034
+ });
1035
+ flushPendingUpdates();
1036
+ }
1037
+
1038
+ overrideContext(['new'], 'value');
1039
+ expect(committedContext).toStrictEqual({
1040
+ array: [1, 2, 3],
1041
+ object: {
1042
+ nested: 'initial',
1043
+ },
1044
+ shallow: 'initial',
1045
+ new: 'value',
1046
+ });
1047
+
1048
+ overrideContext(['object', 'new'], 'value');
1049
+ expect(committedContext).toStrictEqual({
1050
+ array: [1, 2, 3],
1051
+ object: {
1052
+ nested: 'initial',
1053
+ new: 'value',
1054
+ },
1055
+ shallow: 'initial',
1056
+ new: 'value',
1057
+ });
1058
+
1059
+ overrideContext(['array', 3], 'new value');
1060
+ expect(committedContext).toStrictEqual({
1061
+ array: [1, 2, 3, 'new value'],
1062
+ object: {
1063
+ nested: 'initial',
1064
+ new: 'value',
1065
+ },
1066
+ shallow: 'initial',
1067
+ new: 'value',
1068
+ });
1069
+ });
1070
+
1071
+ it('should have deletable keys', async () => {
1072
+ await mountTestApp();
1073
+
1074
+ function deletePath(path) {
1075
+ const rendererID = utils.getRendererID();
1076
+
1077
+ // To simplify hydration and display of primitive context values (e.g. number, string)
1078
+ // the inspectElement() method wraps context in a {value: ...} object.
1079
+ path = ['value', ...path];
1080
+
1081
+ bridge.send('deletePath', {
1082
+ id,
1083
+ path,
1084
+ rendererID,
1085
+ type: 'context',
1086
+ });
1087
+ flushPendingUpdates();
1088
+ }
1089
+
1090
+ deletePath(['shallow']);
1091
+ expect(committedContext).toStrictEqual({
1092
+ array: [1, 2, 3],
1093
+ object: {
1094
+ nested: 'initial',
1095
+ },
1096
+ });
1097
+
1098
+ deletePath(['object', 'nested']);
1099
+ expect(committedContext).toStrictEqual({
1100
+ array: [1, 2, 3],
1101
+ object: {},
1102
+ });
1103
+
1104
+ deletePath(['array', 1]);
1105
+ expect(committedContext).toStrictEqual({
1106
+ array: [1, 3],
1107
+ object: {},
1108
+ });
1109
+ });
1110
+ });
1111
+});