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.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. /*jslint evil: true */
    
  11. 
    
  12. 'use strict';
    
  13. 
    
  14. import * as React from 'react';
    
  15. 
    
  16. import * as ReactART from 'react-art';
    
  17. import ARTSVGMode from 'art/modes/svg';
    
  18. import ARTCurrentMode from 'art/modes/current';
    
  19. // Since these are default exports, we need to import them using ESM.
    
  20. // Since they must be on top, we need to import this before ReactDOM.
    
  21. import Circle from 'react-art/Circle';
    
  22. import Rectangle from 'react-art/Rectangle';
    
  23. import Wedge from 'react-art/Wedge';
    
  24. 
    
  25. // Isolate DOM renderer.
    
  26. jest.resetModules();
    
  27. const ReactDOM = require('react-dom');
    
  28. const ReactTestUtils = require('react-dom/test-utils');
    
  29. 
    
  30. // Isolate test renderer.
    
  31. jest.resetModules();
    
  32. const ReactTestRenderer = require('react-test-renderer');
    
  33. 
    
  34. // Isolate the noop renderer
    
  35. jest.resetModules();
    
  36. const ReactNoop = require('react-noop-renderer');
    
  37. const Scheduler = require('scheduler');
    
  38. 
    
  39. let Group;
    
  40. let Shape;
    
  41. let Surface;
    
  42. let TestComponent;
    
  43. 
    
  44. let waitFor;
    
  45. 
    
  46. const Missing = {};
    
  47. 
    
  48. function testDOMNodeStructure(domNode, expectedStructure) {
    
  49.   expect(domNode).toBeDefined();
    
  50.   expect(domNode.nodeName).toBe(expectedStructure.nodeName);
    
  51.   for (const prop in expectedStructure) {
    
  52.     if (!expectedStructure.hasOwnProperty(prop)) {
    
  53.       continue;
    
  54.     }
    
  55.     if (prop !== 'nodeName' && prop !== 'children') {
    
  56.       if (expectedStructure[prop] === Missing) {
    
  57.         expect(domNode.hasAttribute(prop)).toBe(false);
    
  58.       } else {
    
  59.         expect(domNode.getAttribute(prop)).toBe(expectedStructure[prop]);
    
  60.       }
    
  61.     }
    
  62.   }
    
  63.   if (expectedStructure.children) {
    
  64.     expectedStructure.children.forEach(function (subTree, index) {
    
  65.       testDOMNodeStructure(domNode.childNodes[index], subTree);
    
  66.     });
    
  67.   }
    
  68. }
    
  69. 
    
  70. describe('ReactART', () => {
    
  71.   let container;
    
  72. 
    
  73.   beforeEach(() => {
    
  74.     container = document.createElement('div');
    
  75.     document.body.appendChild(container);
    
  76. 
    
  77.     ARTCurrentMode.setCurrent(ARTSVGMode);
    
  78. 
    
  79.     Group = ReactART.Group;
    
  80.     Shape = ReactART.Shape;
    
  81.     Surface = ReactART.Surface;
    
  82. 
    
  83.     ({waitFor} = require('internal-test-utils'));
    
  84. 
    
  85.     TestComponent = class extends React.Component {
    
  86.       group = React.createRef();
    
  87. 
    
  88.       render() {
    
  89.         const a = (
    
  90.           <Shape
    
  91.             d="M0,0l50,0l0,50l-50,0z"
    
  92.             fill={new ReactART.LinearGradient(['black', 'white'])}
    
  93.             key="a"
    
  94.             width={50}
    
  95.             height={50}
    
  96.             x={50}
    
  97.             y={50}
    
  98.             opacity={0.1}
    
  99.           />
    
  100.         );
    
  101. 
    
  102.         const b = (
    
  103.           <Shape
    
  104.             fill="#3C5A99"
    
  105.             key="b"
    
  106.             scale={0.5}
    
  107.             x={50}
    
  108.             y={50}
    
  109.             title="This is an F"
    
  110.             cursor="pointer">
    
  111.             M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666
    
  112.             h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994
    
  113.             h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z
    
  114.           </Shape>
    
  115.         );
    
  116. 
    
  117.         const c = <Group key="c" />;
    
  118. 
    
  119.         return (
    
  120.           <Surface width={150} height={200}>
    
  121.             <Group ref={this.group}>
    
  122.               {this.props.flipped ? [b, a, c] : [a, b, c]}
    
  123.             </Group>
    
  124.           </Surface>
    
  125.         );
    
  126.       }
    
  127.     };
    
  128.   });
    
  129. 
    
  130.   afterEach(() => {
    
  131.     document.body.removeChild(container);
    
  132.     container = null;
    
  133.   });
    
  134. 
    
  135.   it('should have the correct lifecycle state', () => {
    
  136.     let instance = <TestComponent />;
    
  137.     instance = ReactTestUtils.renderIntoDocument(instance);
    
  138.     const group = instance.group.current;
    
  139.     // Duck type test for an ART group
    
  140.     expect(typeof group.indicate).toBe('function');
    
  141.   });
    
  142. 
    
  143.   it('should render a reasonable SVG structure in SVG mode', () => {
    
  144.     let instance = <TestComponent />;
    
  145.     instance = ReactTestUtils.renderIntoDocument(instance);
    
  146. 
    
  147.     const expectedStructure = {
    
  148.       nodeName: 'svg',
    
  149.       width: '150',
    
  150.       height: '200',
    
  151.       children: [
    
  152.         {nodeName: 'defs'},
    
  153.         {
    
  154.           nodeName: 'g',
    
  155.           children: [
    
  156.             {
    
  157.               nodeName: 'defs',
    
  158.               children: [{nodeName: 'linearGradient'}],
    
  159.             },
    
  160.             {nodeName: 'path'},
    
  161.             {nodeName: 'path'},
    
  162.             {nodeName: 'g'},
    
  163.           ],
    
  164.         },
    
  165.       ],
    
  166.     };
    
  167. 
    
  168.     const realNode = ReactDOM.findDOMNode(instance);
    
  169.     testDOMNodeStructure(realNode, expectedStructure);
    
  170.   });
    
  171. 
    
  172.   it('should be able to reorder components', () => {
    
  173.     const instance = ReactDOM.render(
    
  174.       <TestComponent flipped={false} />,
    
  175.       container,
    
  176.     );
    
  177. 
    
  178.     const expectedStructure = {
    
  179.       nodeName: 'svg',
    
  180.       children: [
    
  181.         {nodeName: 'defs'},
    
  182.         {
    
  183.           nodeName: 'g',
    
  184.           children: [
    
  185.             {nodeName: 'defs'},
    
  186.             {nodeName: 'path', opacity: '0.1'},
    
  187.             {nodeName: 'path', opacity: Missing},
    
  188.             {nodeName: 'g'},
    
  189.           ],
    
  190.         },
    
  191.       ],
    
  192.     };
    
  193. 
    
  194.     const realNode = ReactDOM.findDOMNode(instance);
    
  195.     testDOMNodeStructure(realNode, expectedStructure);
    
  196. 
    
  197.     ReactDOM.render(<TestComponent flipped={true} />, container);
    
  198. 
    
  199.     const expectedNewStructure = {
    
  200.       nodeName: 'svg',
    
  201.       children: [
    
  202.         {nodeName: 'defs'},
    
  203.         {
    
  204.           nodeName: 'g',
    
  205.           children: [
    
  206.             {nodeName: 'defs'},
    
  207.             {nodeName: 'path', opacity: Missing},
    
  208.             {nodeName: 'path', opacity: '0.1'},
    
  209.             {nodeName: 'g'},
    
  210.           ],
    
  211.         },
    
  212.       ],
    
  213.     };
    
  214. 
    
  215.     testDOMNodeStructure(realNode, expectedNewStructure);
    
  216.   });
    
  217. 
    
  218.   it('should be able to reorder many components', () => {
    
  219.     class Component extends React.Component {
    
  220.       render() {
    
  221.         const chars = this.props.chars.split('');
    
  222.         return (
    
  223.           <Surface>
    
  224.             {chars.map(text => (
    
  225.               <Shape key={text} title={text} />
    
  226.             ))}
    
  227.           </Surface>
    
  228.         );
    
  229.       }
    
  230.     }
    
  231. 
    
  232.     // Mini multi-child stress test: lots of reorders, some adds, some removes.
    
  233.     const before = 'abcdefghijklmnopqrst';
    
  234.     const after = 'mxhpgwfralkeoivcstzy';
    
  235. 
    
  236.     let instance = ReactDOM.render(<Component chars={before} />, container);
    
  237.     const realNode = ReactDOM.findDOMNode(instance);
    
  238.     expect(realNode.textContent).toBe(before);
    
  239. 
    
  240.     instance = ReactDOM.render(<Component chars={after} />, container);
    
  241.     expect(realNode.textContent).toBe(after);
    
  242. 
    
  243.     ReactDOM.unmountComponentAtNode(container);
    
  244.   });
    
  245. 
    
  246.   it('renders composite with lifecycle inside group', () => {
    
  247.     let mounted = false;
    
  248. 
    
  249.     class CustomShape extends React.Component {
    
  250.       render() {
    
  251.         return <Shape />;
    
  252.       }
    
  253. 
    
  254.       componentDidMount() {
    
  255.         mounted = true;
    
  256.       }
    
  257.     }
    
  258. 
    
  259.     ReactTestUtils.renderIntoDocument(
    
  260.       <Surface>
    
  261.         <Group>
    
  262.           <CustomShape />
    
  263.         </Group>
    
  264.       </Surface>,
    
  265.     );
    
  266.     expect(mounted).toBe(true);
    
  267.   });
    
  268. 
    
  269.   it('resolves refs before componentDidMount', () => {
    
  270.     class CustomShape extends React.Component {
    
  271.       render() {
    
  272.         return <Shape />;
    
  273.       }
    
  274.     }
    
  275. 
    
  276.     let ref = null;
    
  277. 
    
  278.     class Outer extends React.Component {
    
  279.       test = React.createRef();
    
  280. 
    
  281.       componentDidMount() {
    
  282.         ref = this.test.current;
    
  283.       }
    
  284. 
    
  285.       render() {
    
  286.         return (
    
  287.           <Surface>
    
  288.             <Group>
    
  289.               <CustomShape ref={this.test} />
    
  290.             </Group>
    
  291.           </Surface>
    
  292.         );
    
  293.       }
    
  294.     }
    
  295. 
    
  296.     ReactTestUtils.renderIntoDocument(<Outer />);
    
  297.     expect(ref.constructor).toBe(CustomShape);
    
  298.   });
    
  299. 
    
  300.   it('resolves refs before componentDidUpdate', () => {
    
  301.     class CustomShape extends React.Component {
    
  302.       render() {
    
  303.         return <Shape />;
    
  304.       }
    
  305.     }
    
  306. 
    
  307.     let ref = {};
    
  308. 
    
  309.     class Outer extends React.Component {
    
  310.       test = React.createRef();
    
  311. 
    
  312.       componentDidMount() {
    
  313.         ref = this.test.current;
    
  314.       }
    
  315. 
    
  316.       componentDidUpdate() {
    
  317.         ref = this.test.current;
    
  318.       }
    
  319. 
    
  320.       render() {
    
  321.         return (
    
  322.           <Surface>
    
  323.             <Group>
    
  324.               {this.props.mountCustomShape && <CustomShape ref={this.test} />}
    
  325.             </Group>
    
  326.           </Surface>
    
  327.         );
    
  328.       }
    
  329.     }
    
  330.     ReactDOM.render(<Outer />, container);
    
  331.     expect(ref).toBe(null);
    
  332.     ReactDOM.render(<Outer mountCustomShape={true} />, container);
    
  333.     expect(ref.constructor).toBe(CustomShape);
    
  334.   });
    
  335. 
    
  336.   it('adds and updates event handlers', () => {
    
  337.     function render(onClick) {
    
  338.       return ReactDOM.render(
    
  339.         <Surface>
    
  340.           <Shape onClick={onClick} />
    
  341.         </Surface>,
    
  342.         container,
    
  343.       );
    
  344.     }
    
  345. 
    
  346.     function doClick(instance) {
    
  347.       const path = ReactDOM.findDOMNode(instance).querySelector('path');
    
  348. 
    
  349.       path.dispatchEvent(
    
  350.         new MouseEvent('click', {
    
  351.           bubbles: true,
    
  352.         }),
    
  353.       );
    
  354.     }
    
  355. 
    
  356.     const onClick1 = jest.fn();
    
  357.     let instance = render(onClick1);
    
  358.     doClick(instance);
    
  359.     expect(onClick1).toBeCalled();
    
  360. 
    
  361.     const onClick2 = jest.fn();
    
  362.     instance = render(onClick2);
    
  363.     doClick(instance);
    
  364.     expect(onClick2).toBeCalled();
    
  365.   });
    
  366. 
    
  367.   // @gate forceConcurrentByDefaultForTesting
    
  368.   it('can concurrently render with a "primary" renderer while sharing context', async () => {
    
  369.     const CurrentRendererContext = React.createContext(null);
    
  370. 
    
  371.     function Yield(props) {
    
  372.       Scheduler.log(props.value);
    
  373.       return null;
    
  374.     }
    
  375. 
    
  376.     let ops = [];
    
  377.     function LogCurrentRenderer() {
    
  378.       return (
    
  379.         <CurrentRendererContext.Consumer>
    
  380.           {currentRenderer => {
    
  381.             ops.push(currentRenderer);
    
  382.             return null;
    
  383.           }}
    
  384.         </CurrentRendererContext.Consumer>
    
  385.       );
    
  386.     }
    
  387. 
    
  388.     // Using test renderer instead of the DOM renderer here because async
    
  389.     // testing APIs for the DOM renderer don't exist.
    
  390.     ReactNoop.render(
    
  391.       <CurrentRendererContext.Provider value="Test">
    
  392.         <Yield value="A" />
    
  393.         <Yield value="B" />
    
  394.         <LogCurrentRenderer />
    
  395.         <Yield value="C" />
    
  396.       </CurrentRendererContext.Provider>,
    
  397.     );
    
  398. 
    
  399.     await waitFor(['A']);
    
  400. 
    
  401.     ReactDOM.render(
    
  402.       <Surface>
    
  403.         <LogCurrentRenderer />
    
  404.         <CurrentRendererContext.Provider value="ART">
    
  405.           <LogCurrentRenderer />
    
  406.         </CurrentRendererContext.Provider>
    
  407.       </Surface>,
    
  408.       container,
    
  409.     );
    
  410. 
    
  411.     expect(ops).toEqual([null, 'ART']);
    
  412. 
    
  413.     ops = [];
    
  414.     await waitFor(['B', 'C']);
    
  415. 
    
  416.     expect(ops).toEqual(['Test']);
    
  417.   });
    
  418. });
    
  419. 
    
  420. describe('ReactARTComponents', () => {
    
  421.   it('should generate a <Shape> with props for drawing the Circle', () => {
    
  422.     const circle = ReactTestRenderer.create(
    
  423.       <Circle radius={10} stroke="green" strokeWidth={3} fill="blue" />,
    
  424.     );
    
  425.     expect(circle.toJSON()).toMatchSnapshot();
    
  426.   });
    
  427. 
    
  428.   it('should warn if radius is missing on a Circle component', () => {
    
  429.     expect(() =>
    
  430.       ReactTestRenderer.create(
    
  431.         <Circle stroke="green" strokeWidth={3} fill="blue" />,
    
  432.       ),
    
  433.     ).toErrorDev(
    
  434.       'Warning: Failed prop type: The prop `radius` is marked as required in `Circle`, ' +
    
  435.         'but its value is `undefined`.' +
    
  436.         '\n    in Circle (at **)',
    
  437.     );
    
  438.   });
    
  439. 
    
  440.   it('should generate a <Shape> with props for drawing the Rectangle', () => {
    
  441.     const rectangle = ReactTestRenderer.create(
    
  442.       <Rectangle width={50} height={50} stroke="green" fill="blue" />,
    
  443.     );
    
  444.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  445.   });
    
  446. 
    
  447.   it('should generate a <Shape> with positive width when width prop is negative', () => {
    
  448.     const rectangle = ReactTestRenderer.create(
    
  449.       <Rectangle width={-50} height={50} />,
    
  450.     );
    
  451.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  452.   });
    
  453. 
    
  454.   it('should generate a <Shape> with positive height when height prop is negative', () => {
    
  455.     const rectangle = ReactTestRenderer.create(
    
  456.       <Rectangle height={-50} width={50} />,
    
  457.     );
    
  458.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  459.   });
    
  460. 
    
  461.   it('should generate a <Shape> with a radius property of 0 when top left radius prop is negative', () => {
    
  462.     const rectangle = ReactTestRenderer.create(
    
  463.       <Rectangle radiusTopLeft={-25} width={50} height={50} />,
    
  464.     );
    
  465.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  466.   });
    
  467. 
    
  468.   it('should generate a <Shape> with a radius property of 0 when top right radius prop is negative', () => {
    
  469.     const rectangle = ReactTestRenderer.create(
    
  470.       <Rectangle radiusTopRight={-25} width={50} height={50} />,
    
  471.     );
    
  472.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  473.   });
    
  474. 
    
  475.   it('should generate a <Shape> with a radius property of 0 when bottom right radius prop is negative', () => {
    
  476.     const rectangle = ReactTestRenderer.create(
    
  477.       <Rectangle radiusBottomRight={-30} width={50} height={50} />,
    
  478.     );
    
  479.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  480.   });
    
  481. 
    
  482.   it('should generate a <Shape> with a radius property of 0 when bottom left radius prop is negative', () => {
    
  483.     const rectangle = ReactTestRenderer.create(
    
  484.       <Rectangle radiusBottomLeft={-25} width={50} height={50} />,
    
  485.     );
    
  486.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  487.   });
    
  488. 
    
  489.   it('should generate a <Shape> where top radius is 0 if the sum of the top radius is greater than width', () => {
    
  490.     const rectangle = ReactTestRenderer.create(
    
  491.       <Rectangle
    
  492.         radiusTopRight={25}
    
  493.         radiusTopLeft={26}
    
  494.         width={50}
    
  495.         height={40}
    
  496.       />,
    
  497.     );
    
  498.     expect(rectangle.toJSON()).toMatchSnapshot();
    
  499.   });
    
  500. 
    
  501.   it('should warn if width/height is missing on a Rectangle component', () => {
    
  502.     expect(() =>
    
  503.       ReactTestRenderer.create(<Rectangle stroke="green" fill="blue" />),
    
  504.     ).toErrorDev([
    
  505.       'Warning: Failed prop type: The prop `width` is marked as required in `Rectangle`, ' +
    
  506.         'but its value is `undefined`.' +
    
  507.         '\n    in Rectangle (at **)',
    
  508.       'Warning: Failed prop type: The prop `height` is marked as required in `Rectangle`, ' +
    
  509.         'but its value is `undefined`.' +
    
  510.         '\n    in Rectangle (at **)',
    
  511.     ]);
    
  512.   });
    
  513. 
    
  514.   it('should generate a <Shape> with props for drawing the Wedge', () => {
    
  515.     const wedge = ReactTestRenderer.create(
    
  516.       <Wedge outerRadius={50} startAngle={0} endAngle={360} fill="blue" />,
    
  517.     );
    
  518.     expect(wedge.toJSON()).toMatchSnapshot();
    
  519.   });
    
  520. 
    
  521.   it('should return null if startAngle equals to endAngle on Wedge', () => {
    
  522.     const wedge = ReactTestRenderer.create(
    
  523.       <Wedge outerRadius={50} startAngle={0} endAngle={0} fill="blue" />,
    
  524.     );
    
  525.     expect(wedge.toJSON()).toBeNull();
    
  526.   });
    
  527. 
    
  528.   it('should warn if outerRadius/startAngle/endAngle is missing on a Wedge component', () => {
    
  529.     expect(() => ReactTestRenderer.create(<Wedge fill="blue" />)).toErrorDev([
    
  530.       'Warning: Failed prop type: The prop `outerRadius` is marked as required in `Wedge`, ' +
    
  531.         'but its value is `undefined`.' +
    
  532.         '\n    in Wedge (at **)',
    
  533.       'Warning: Failed prop type: The prop `startAngle` is marked as required in `Wedge`, ' +
    
  534.         'but its value is `undefined`.' +
    
  535.         '\n    in Wedge (at **)',
    
  536.       'Warning: Failed prop type: The prop `endAngle` is marked as required in `Wedge`, ' +
    
  537.         'but its value is `undefined`.' +
    
  538.         '\n    in Wedge (at **)',
    
  539.     ]);
    
  540.   });
    
  541. });