main
js 944 lines 23.6 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('InspectedElementContext', () => {
14 let React;
15 let ReactDOM;
16 let bridge: FrontendBridge;
17 let store: Store;
18
19 let backendAPI;
20
21 const act = (callback: Function) => {
22 callback();
23
24 jest.runAllTimers(); // Flush Bridge operations
25 };
26
27 async function read(
28 id: number,
29 path: Array<string | number> = null,
30 ): Promise<Object> {
31 const rendererID = ((store.getRendererIDForElement(id): any): number);
32 const promise = backendAPI
33 .inspectElement(bridge, false, id, path, rendererID)
34 .then(data =>
35 backendAPI.convertInspectedElementBackendToFrontend(data.value),
36 );
37
38 jest.runOnlyPendingTimers();
39
40 return promise;
41 }
42
43 beforeEach(() => {
44 bridge = global.bridge;
45 store = global.store;
46
47 backendAPI = require('react-devtools-shared/src/backendAPI');
48
49 // Redirect all React/ReactDOM requires to the v15 UMD.
50 // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
51 jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
52 jest.mock('react-dom', () =>
53 jest.requireActual('react-dom-15/dist/react-dom.js'),
54 );
55
56 React = require('react');
57 ReactDOM = require('react-dom');
58 });
59
60 // @reactVersion >= 16.0
61 it('should inspect the currently selected element', async () => {
62 const Example = () => null;
63
64 act(() =>
65 ReactDOM.render(
66 React.createElement(Example, {a: 1, b: 'abc'}),
67 document.createElement('div'),
68 ),
69 );
70
71 const id = ((store.getElementIDAtIndex(0): any): number);
72 const inspectedElement = await read(id);
73
74 expect(inspectedElement).toMatchInlineSnapshot(`
75 {
76 "context": {},
77 "events": undefined,
78 "hooks": null,
79 "id": 2,
80 "owners": null,
81 "props": {
82 "a": 1,
83 "b": "abc",
84 },
85 "rootType": null,
86 "state": null,
87 }
88 `);
89 });
90
91 // @reactVersion >= 16.0
92 it('should support simple data types', async () => {
93 const Example = () => null;
94
95 act(() =>
96 ReactDOM.render(
97 React.createElement(Example, {
98 boolean_false: false,
99 boolean_true: true,
100 infinity: Infinity,
101 minus_infinity: -Infinity,
102 integer_zero: 0,
103 integer_one: 1,
104 float: 1.23,
105 string: 'abc',
106 string_empty: '',
107 nan: NaN,
108 value_null: null,
109 value_undefined: undefined,
110 }),
111 document.createElement('div'),
112 ),
113 );
114
115 const id = ((store.getElementIDAtIndex(0): any): number);
116 const inspectedElement = await read(id);
117
118 expect(inspectedElement).toMatchInlineSnapshot(`
119 {
120 "context": {},
121 "events": undefined,
122 "hooks": null,
123 "id": 2,
124 "owners": null,
125 "props": {
126 "boolean_false": false,
127 "boolean_true": true,
128 "float": 1.23,
129 "infinity": Infinity,
130 "integer_one": 1,
131 "integer_zero": 0,
132 "minus_infinity": -Infinity,
133 "nan": NaN,
134 "string": "abc",
135 "string_empty": "",
136 "value_null": null,
137 "value_undefined": undefined,
138 },
139 "rootType": null,
140 "state": null,
141 }
142 `);
143 });
144
145 // @reactVersion >= 16.0
146 it('should support complex data types', async () => {
147 const Immutable = require('immutable');
148
149 const Example = () => null;
150
151 const arrayOfArrays = [[['abc', 123, true], []]];
152 const div = document.createElement('div');
153 const exampleFunction = () => {};
154 const setShallow = new Set(['abc', 123]);
155 const mapShallow = new Map([
156 ['name', 'Brian'],
157 ['food', 'sushi'],
158 ]);
159 const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
160 const mapOfMaps = new Map([
161 ['first', mapShallow],
162 ['second', mapShallow],
163 ]);
164 const objectOfObjects = {
165 inner: {string: 'abc', number: 123, boolean: true},
166 };
167 const typedArray = Int8Array.from([100, -100, 0]);
168 const arrayBuffer = typedArray.buffer;
169 const dataView = new DataView(arrayBuffer);
170 const immutableMap = Immutable.fromJS({
171 a: [{hello: 'there'}, 'fixed', true],
172 b: 123,
173 c: {
174 '1': 'xyz',
175 xyz: 1,
176 },
177 });
178
179 class Class {
180 anonymousFunction = () => {};
181 }
182 const instance = new Class();
183
184 act(() =>
185 ReactDOM.render(
186 React.createElement(Example, {
187 anonymous_fn: instance.anonymousFunction,
188 array_buffer: arrayBuffer,
189 array_of_arrays: arrayOfArrays,
190 big_int: BigInt(123),
191 bound_fn: exampleFunction.bind(this),
192 data_view: dataView,
193 date: new Date(123),
194 fn: exampleFunction,
195 html_element: div,
196 immutable: immutableMap,
197 map: mapShallow,
198 map_of_maps: mapOfMaps,
199 object_of_objects: objectOfObjects,
200 react_element: React.createElement('span'),
201 regexp: /abc/giu,
202 set: setShallow,
203 set_of_sets: setOfSets,
204 symbol: Symbol('symbol'),
205 typed_array: typedArray,
206 }),
207 document.createElement('div'),
208 ),
209 );
210
211 const id = ((store.getElementIDAtIndex(0): any): number);
212 const inspectedElement = await read(id);
213
214 expect(inspectedElement.props).toMatchInlineSnapshot(`
215 {
216 "anonymous_fn": Dehydrated {
217 "preview_short": () => {},
218 "preview_long": () => {},
219 },
220 "array_buffer": Dehydrated {
221 "preview_short": ArrayBuffer(3),
222 "preview_long": ArrayBuffer(3),
223 },
224 "array_of_arrays": [
225 Dehydrated {
226 "preview_short": Array(2),
227 "preview_long": [Array(3), Array(0)],
228 },
229 ],
230 "big_int": Dehydrated {
231 "preview_short": 123n,
232 "preview_long": 123n,
233 },
234 "bound_fn": Dehydrated {
235 "preview_short": bound exampleFunction() {},
236 "preview_long": bound exampleFunction() {},
237 },
238 "data_view": Dehydrated {
239 "preview_short": DataView(3),
240 "preview_long": DataView(3),
241 },
242 "date": Dehydrated {
243 "preview_short": Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time),
244 "preview_long": Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time),
245 },
246 "fn": Dehydrated {
247 "preview_short": exampleFunction() {},
248 "preview_long": exampleFunction() {},
249 },
250 "html_element": Dehydrated {
251 "preview_short": <div />,
252 "preview_long": <div />,
253 },
254 "immutable": {
255 "0": Dehydrated {
256 "preview_short": Array(2),
257 "preview_long": ["a", List(3)],
258 },
259 "1": Dehydrated {
260 "preview_short": Array(2),
261 "preview_long": ["b", 123],
262 },
263 "2": Dehydrated {
264 "preview_short": Array(2),
265 "preview_long": ["c", Map(2)],
266 },
267 },
268 "map": {
269 "0": Dehydrated {
270 "preview_short": Array(2),
271 "preview_long": ["name", "Brian"],
272 },
273 "1": Dehydrated {
274 "preview_short": Array(2),
275 "preview_long": ["food", "sushi"],
276 },
277 },
278 "map_of_maps": {
279 "0": Dehydrated {
280 "preview_short": Array(2),
281 "preview_long": ["first", Map(2)],
282 },
283 "1": Dehydrated {
284 "preview_short": Array(2),
285 "preview_long": ["second", Map(2)],
286 },
287 },
288 "object_of_objects": {
289 "inner": Dehydrated {
290 "preview_short": {…},
291 "preview_long": {boolean: true, number: 123, string: "abc"},
292 },
293 },
294 "react_element": {
295 "key": null,
296 "props": Dehydrated {
297 "preview_short": {…},
298 "preview_long": {},
299 },
300 "ref": null,
301 },
302 "regexp": Dehydrated {
303 "preview_short": /abc/giu,
304 "preview_long": /abc/giu,
305 },
306 "set": {
307 "0": "abc",
308 "1": 123,
309 },
310 "set_of_sets": {
311 "0": Dehydrated {
312 "preview_short": Set(3),
313 "preview_long": Set(3) {"a", "b", "c"},
314 },
315 "1": Dehydrated {
316 "preview_short": Set(3),
317 "preview_long": Set(3) {1, 2, 3},
318 },
319 },
320 "symbol": Dehydrated {
321 "preview_short": Symbol(symbol),
322 "preview_long": Symbol(symbol),
323 },
324 "typed_array": {
325 "0": 100,
326 "1": -100,
327 "2": 0,
328 },
329 }
330 `);
331 });
332
333 // @reactVersion >= 16.0
334 it('should support objects with no prototype', async () => {
335 const Example = () => null;
336
337 const object = Object.create(null);
338 object.string = 'abc';
339 object.number = 123;
340 object.boolean = true;
341
342 act(() =>
343 ReactDOM.render(
344 React.createElement(Example, {object}),
345 document.createElement('div'),
346 ),
347 );
348
349 const id = ((store.getElementIDAtIndex(0): any): number);
350 const inspectedElement = await read(id);
351
352 expect(inspectedElement.props).toMatchInlineSnapshot(`
353 {
354 "object": {
355 "boolean": true,
356 "number": 123,
357 "string": "abc",
358 },
359 }
360 `);
361 });
362
363 // @reactVersion >= 16.0
364 it('should support objects with overridden hasOwnProperty', async () => {
365 const Example = () => null;
366
367 const object = {
368 name: 'blah',
369 hasOwnProperty: true,
370 };
371
372 act(() =>
373 ReactDOM.render(
374 React.createElement(Example, {object}),
375 document.createElement('div'),
376 ),
377 );
378
379 const id = ((store.getElementIDAtIndex(0): any): number);
380 const inspectedElement = await read(id);
381
382 // TRICKY: Don't use toMatchInlineSnapshot() for this test!
383 // Our snapshot serializer relies on hasOwnProperty() for feature detection.
384 expect(inspectedElement.props.object.name).toBe('blah');
385 expect(inspectedElement.props.object.hasOwnProperty).toBe(true);
386 });
387
388 // @reactVersion >= 16.0
389 it('should not consume iterables while inspecting', async () => {
390 const Example = () => null;
391
392 function* generator() {
393 yield 1;
394 yield 2;
395 }
396
397 const iteratable = generator();
398
399 act(() =>
400 ReactDOM.render(
401 React.createElement(Example, {iteratable}),
402 document.createElement('div'),
403 ),
404 );
405
406 const id = ((store.getElementIDAtIndex(0): any): number);
407 const inspectedElement = await read(id);
408
409 expect(inspectedElement).toMatchInlineSnapshot(`
410 {
411 "context": {},
412 "events": undefined,
413 "hooks": null,
414 "id": 2,
415 "owners": null,
416 "props": {
417 "iteratable": Dehydrated {
418 "preview_short": Generator,
419 "preview_long": Generator,
420 },
421 },
422 "rootType": null,
423 "state": null,
424 }
425 `);
426
427 // Inspecting should not consume the iterable.
428 expect(iteratable.next().value).toEqual(1);
429 expect(iteratable.next().value).toEqual(2);
430 expect(iteratable.next().value).toBeUndefined();
431 });
432
433 // @reactVersion >= 16.0
434 it('should support custom objects with enumerable properties and getters', async () => {
435 class CustomData {
436 _number = 42;
437 get number() {
438 return this._number;
439 }
440 set number(value) {
441 this._number = value;
442 }
443 }
444
445 const descriptor = ((Object.getOwnPropertyDescriptor(
446 CustomData.prototype,
447 'number',
448 ): any): PropertyDescriptor<number>);
449 descriptor.enumerable = true;
450 Object.defineProperty(CustomData.prototype, 'number', descriptor);
451
452 const Example = ({data}) => null;
453
454 act(() =>
455 ReactDOM.render(
456 React.createElement(Example, {data: new CustomData()}),
457 document.createElement('div'),
458 ),
459 );
460
461 const id = ((store.getElementIDAtIndex(0): any): number);
462 const inspectedElement = await read(id);
463
464 expect(inspectedElement).toMatchInlineSnapshot(`
465 {
466 "context": {},
467 "events": undefined,
468 "hooks": null,
469 "id": 2,
470 "owners": null,
471 "props": {
472 "data": {
473 "_number": 42,
474 "number": 42,
475 },
476 },
477 "rootType": null,
478 "state": null,
479 }
480 `);
481 });
482
483 // @reactVersion >= 16.0
484 it('should support objects with inherited keys', async () => {
485 const Example = () => null;
486
487 const base = Object.create(Object.prototype, {
488 enumerableStringBase: {
489 value: 1,
490 writable: true,
491 enumerable: true,
492 configurable: true,
493 },
494 [Symbol('enumerableSymbolBase')]: {
495 value: 1,
496 writable: true,
497 enumerable: true,
498 configurable: true,
499 },
500 nonEnumerableStringBase: {
501 value: 1,
502 writable: true,
503 enumerable: false,
504 configurable: true,
505 },
506 [Symbol('nonEnumerableSymbolBase')]: {
507 value: 1,
508 writable: true,
509 enumerable: false,
510 configurable: true,
511 },
512 });
513
514 const object = Object.create(base, {
515 enumerableString: {
516 value: 2,
517 writable: true,
518 enumerable: true,
519 configurable: true,
520 },
521 nonEnumerableString: {
522 value: 3,
523 writable: true,
524 enumerable: false,
525 configurable: true,
526 },
527 123: {
528 value: 3,
529 writable: true,
530 enumerable: true,
531 configurable: true,
532 },
533 [Symbol('nonEnumerableSymbol')]: {
534 value: 2,
535 writable: true,
536 enumerable: false,
537 configurable: true,
538 },
539 [Symbol('enumerableSymbol')]: {
540 value: 3,
541 writable: true,
542 enumerable: true,
543 configurable: true,
544 },
545 });
546
547 act(() =>
548 ReactDOM.render(
549 React.createElement(Example, {data: object}),
550 document.createElement('div'),
551 ),
552 );
553
554 const id = ((store.getElementIDAtIndex(0): any): number);
555 const inspectedElement = await read(id);
556
557 expect(inspectedElement).toMatchInlineSnapshot(`
558 {
559 "context": {},
560 "events": undefined,
561 "hooks": null,
562 "id": 2,
563 "owners": null,
564 "props": {
565 "data": {
566 "123": 3,
567 "Symbol(enumerableSymbol)": 3,
568 "Symbol(enumerableSymbolBase)": 1,
569 "enumerableString": 2,
570 "enumerableStringBase": 1,
571 },
572 },
573 "rootType": null,
574 "state": null,
575 }
576 `);
577 });
578
579 // @reactVersion >= 16.0
580 it('should allow component prop value and value`s prototype has same name params.', async () => {
581 const testData = Object.create(
582 {
583 a: undefined,
584 b: Infinity,
585 c: NaN,
586 d: 'normal',
587 },
588 {
589 a: {
590 value: undefined,
591 writable: true,
592 enumerable: true,
593 configurable: true,
594 },
595 b: {
596 value: Infinity,
597 writable: true,
598 enumerable: true,
599 configurable: true,
600 },
601 c: {
602 value: NaN,
603 writable: true,
604 enumerable: true,
605 configurable: true,
606 },
607 d: {
608 value: 'normal',
609 writable: true,
610 enumerable: true,
611 configurable: true,
612 },
613 },
614 );
615
616 const Example = ({data}) => null;
617 act(() =>
618 ReactDOM.render(
619 React.createElement(Example, {data: testData}),
620 document.createElement('div'),
621 ),
622 );
623
624 const id = ((store.getElementIDAtIndex(0): any): number);
625 const inspectedElement = await read(id);
626
627 expect(inspectedElement.props).toMatchInlineSnapshot(`
628 {
629 "data": {
630 "a": undefined,
631 "b": Infinity,
632 "c": NaN,
633 "d": "normal",
634 },
635 }
636 `);
637 });
638
639 // @reactVersion >= 16.0
640 it('should not dehydrate nested values until explicitly requested', async () => {
641 const Example = () => null;
642
643 act(() =>
644 ReactDOM.render(
645 React.createElement(Example, {
646 nestedObject: {
647 a: {
648 b: {
649 c: [
650 {
651 d: {
652 e: {},
653 },
654 },
655 ],
656 },
657 },
658 },
659 }),
660 document.createElement('div'),
661 ),
662 );
663
664 const id = ((store.getElementIDAtIndex(0): any): number);
665
666 let inspectedElement = await read(id);
667 expect(inspectedElement.props).toMatchInlineSnapshot(`
668 {
669 "nestedObject": {
670 "a": Dehydrated {
671 "preview_short": {…},
672 "preview_long": {b: {…}},
673 },
674 },
675 }
676 `);
677
678 inspectedElement = await read(id, ['props', 'nestedObject', 'a']);
679 expect(inspectedElement.props).toMatchInlineSnapshot(`
680 {
681 "nestedObject": {
682 "a": {
683 "b": {
684 "c": Dehydrated {
685 "preview_short": Array(1),
686 "preview_long": [{…}],
687 },
688 },
689 },
690 },
691 }
692 `);
693
694 inspectedElement = await read(id, ['props', 'nestedObject', 'a', 'b', 'c']);
695 expect(inspectedElement.props).toMatchInlineSnapshot(`
696 {
697 "nestedObject": {
698 "a": {
699 "b": {
700 "c": [
701 {
702 "d": Dehydrated {
703 "preview_short": {…},
704 "preview_long": {e: {…}},
705 },
706 },
707 ],
708 },
709 },
710 },
711 }
712 `);
713
714 inspectedElement = await read(id, [
715 'props',
716 'nestedObject',
717 'a',
718 'b',
719 'c',
720 0,
721 'd',
722 ]);
723 expect(inspectedElement.props).toMatchInlineSnapshot(`
724 {
725 "nestedObject": {
726 "a": {
727 "b": {
728 "c": [
729 {
730 "d": {
731 "e": {},
732 },
733 },
734 ],
735 },
736 },
737 },
738 }
739 `);
740 });
741
742 // @reactVersion >= 16.0
743 it('should enable inspected values to be stored as global variables', () => {
744 const Example = () => null;
745
746 const nestedObject = {
747 a: {
748 value: 1,
749 b: {
750 value: 1,
751 c: {
752 value: 1,
753 },
754 },
755 },
756 };
757
758 act(() =>
759 ReactDOM.render(
760 React.createElement(Example, {nestedObject}),
761 document.createElement('div'),
762 ),
763 );
764
765 const id = ((store.getElementIDAtIndex(0): any): number);
766 const rendererID = ((store.getRendererIDForElement(id): any): number);
767
768 const logSpy = jest.fn();
769 jest.spyOn(console, 'log').mockImplementation(logSpy);
770
771 // Should store the whole value (not just the hydrated parts)
772 backendAPI.storeAsGlobal({
773 bridge,
774 id,
775 path: ['props', 'nestedObject'],
776 rendererID,
777 });
778
779 jest.runOnlyPendingTimers();
780 expect(logSpy).toHaveBeenCalledWith('$reactTemp0');
781 expect(global.$reactTemp0).toBe(nestedObject);
782
783 logSpy.mockReset();
784
785 // Should store the nested property specified (not just the outer value)
786 backendAPI.storeAsGlobal({
787 bridge,
788 id,
789 path: ['props', 'nestedObject', 'a', 'b'],
790 rendererID,
791 });
792
793 jest.runOnlyPendingTimers();
794 expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
795 expect(global.$reactTemp1).toBe(nestedObject.a.b);
796 });
797
798 // @reactVersion >= 16.0
799 it('should enable inspected values to be copied to the clipboard', () => {
800 const Example = () => null;
801
802 const nestedObject = {
803 a: {
804 value: 1,
805 b: {
806 value: 1,
807 c: {
808 value: 1,
809 },
810 },
811 },
812 };
813
814 act(() =>
815 ReactDOM.render(
816 React.createElement(Example, {nestedObject}),
817 document.createElement('div'),
818 ),
819 );
820
821 const id = ((store.getElementIDAtIndex(0): any): number);
822 const rendererID = ((store.getRendererIDForElement(id): any): number);
823
824 // Should copy the whole value (not just the hydrated parts)
825 backendAPI.copyInspectedElementPath({
826 bridge,
827 id,
828 path: ['props', 'nestedObject'],
829 rendererID,
830 });
831
832 jest.runOnlyPendingTimers();
833 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
834 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
835 JSON.stringify(nestedObject, undefined, 2),
836 );
837
838 global.mockClipboardCopy.mockReset();
839
840 // Should copy the nested property specified (not just the outer value)
841 backendAPI.copyInspectedElementPath({
842 bridge,
843 id,
844 path: ['props', 'nestedObject', 'a', 'b'],
845 rendererID,
846 });
847
848 jest.runOnlyPendingTimers();
849 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
850 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
851 JSON.stringify(nestedObject.a.b, undefined, 2),
852 );
853 });
854
855 // @reactVersion >= 16.0
856 it('should enable complex values to be copied to the clipboard', () => {
857 const Immutable = require('immutable');
858
859 const Example = () => null;
860
861 const set = new Set(['abc', 123]);
862 const map = new Map([
863 ['name', 'Brian'],
864 ['food', 'sushi'],
865 ]);
866 const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
867 const mapOfMaps = new Map([
868 ['first', map],
869 ['second', map],
870 ]);
871 const typedArray = Int8Array.from([100, -100, 0]);
872 const arrayBuffer = typedArray.buffer;
873 const dataView = new DataView(arrayBuffer);
874 const immutable = Immutable.fromJS({
875 a: [{hello: 'there'}, 'fixed', true],
876 b: 123,
877 c: {
878 '1': 'xyz',
879 xyz: 1,
880 },
881 });
882 const bigInt = BigInt(123);
883
884 act(() =>
885 ReactDOM.render(
886 React.createElement(Example, {
887 arrayBuffer: arrayBuffer,
888 dataView: dataView,
889 map: map,
890 set: set,
891 mapOfMaps: mapOfMaps,
892 setOfSets: setOfSets,
893 typedArray: typedArray,
894 immutable: immutable,
895 bigInt: bigInt,
896 }),
897 document.createElement('div'),
898 ),
899 );
900
901 const id = ((store.getElementIDAtIndex(0): any): number);
902 const rendererID = ((store.getRendererIDForElement(id): any): number);
903
904 // Should copy the whole value (not just the hydrated parts)
905 backendAPI.copyInspectedElementPath({
906 bridge,
907 id,
908 path: ['props'],
909 rendererID,
910 });
911 jest.runOnlyPendingTimers();
912 // Should not error despite lots of unserialized values.
913
914 global.mockClipboardCopy.mockReset();
915
916 // Should copy the nested property specified (not just the outer value)
917 backendAPI.copyInspectedElementPath({
918 bridge,
919 id,
920 path: ['props', 'bigInt'],
921 rendererID,
922 });
923 jest.runOnlyPendingTimers();
924 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
925 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
926 JSON.stringify('123n'),
927 );
928
929 global.mockClipboardCopy.mockReset();
930
931 // Should copy the nested property specified (not just the outer value)
932 backendAPI.copyInspectedElementPath({
933 bridge,
934 id,
935 path: ['props', 'typedArray'],
936 rendererID,
937 });
938 jest.runOnlyPendingTimers();
939 expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
940 expect(global.mockClipboardCopy).toHaveBeenCalledWith(
941 JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
942 );
943 });
944 });