main
js 530 lines 13.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 * @emails react-core
8 */
9
10 /*jslint evil: true */
11
12 'use strict';
13
14 const React = require('react');
15 const Scheduler = require('scheduler');
16
17 import * as ReactART from 'react-art';
18 import ARTSVGMode from 'art/modes/svg';
19 import ARTCurrentMode from 'art/modes/current';
20 // Since these are default exports, we need to import them using ESM.
21 // Since they must be on top, we need to import this before ReactDOM.
22 import Circle from 'react-art/Circle';
23 import Rectangle from 'react-art/Rectangle';
24 import Wedge from 'react-art/Wedge';
25
26 const {act} = require('internal-test-utils');
27
28 // Isolate DOM renderer.
29 jest.resetModules();
30 // share isomorphic
31 jest.mock('scheduler', () => Scheduler);
32 jest.mock('react', () => React);
33 const ReactDOMClient = require('react-dom/client');
34
35 let Group;
36 let Shape;
37 let Surface;
38 let TestComponent;
39
40 let groupRef;
41
42 const Missing = {};
43
44 function testDOMNodeStructure(domNode, expectedStructure) {
45 expect(domNode).toBeDefined();
46 expect(domNode.nodeName).toBe(expectedStructure.nodeName);
47 for (const prop in expectedStructure) {
48 if (!expectedStructure.hasOwnProperty(prop)) {
49 continue;
50 }
51 if (prop !== 'nodeName' && prop !== 'children') {
52 if (expectedStructure[prop] === Missing) {
53 expect(domNode.hasAttribute(prop)).toBe(false);
54 } else {
55 expect(domNode.getAttribute(prop)).toBe(expectedStructure[prop]);
56 }
57 }
58 }
59 if (expectedStructure.children) {
60 expectedStructure.children.forEach(function (subTree, index) {
61 testDOMNodeStructure(domNode.childNodes[index], subTree);
62 });
63 }
64 }
65
66 describe('ReactART', () => {
67 let container;
68
69 beforeEach(() => {
70 jest.resetModules();
71 // share isomorphic
72 jest.mock('scheduler', () => Scheduler);
73 jest.mock('react', () => React);
74
75 container = document.createElement('div');
76 document.body.appendChild(container);
77
78 ARTCurrentMode.setCurrent(ARTSVGMode);
79
80 Group = ReactART.Group;
81 Shape = ReactART.Shape;
82 Surface = ReactART.Surface;
83
84 groupRef = React.createRef();
85 TestComponent = class extends React.Component {
86 group = groupRef;
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', async () => {
136 const instance = <TestComponent />;
137 const root = ReactDOMClient.createRoot(container);
138 await act(() => {
139 root.render(instance);
140 });
141 const group = groupRef.current;
142 // Duck type test for an ART group
143 expect(typeof group.indicate).toBe('function');
144 });
145
146 it('should render a reasonable SVG structure in SVG mode', async () => {
147 const instance = <TestComponent />;
148 const root = ReactDOMClient.createRoot(container);
149 await act(() => {
150 root.render(instance);
151 });
152
153 const expectedStructure = {
154 nodeName: 'svg',
155 width: '150',
156 height: '200',
157 children: [
158 {nodeName: 'defs'},
159 {
160 nodeName: 'g',
161 children: [
162 {
163 nodeName: 'defs',
164 children: [{nodeName: 'linearGradient'}],
165 },
166 {nodeName: 'path'},
167 {nodeName: 'path'},
168 {nodeName: 'g'},
169 ],
170 },
171 ],
172 };
173
174 const realNode = container.firstChild;
175 testDOMNodeStructure(realNode, expectedStructure);
176 });
177
178 it('should be able to reorder components', async () => {
179 const root = ReactDOMClient.createRoot(container);
180 await act(() => {
181 root.render(<TestComponent flipped={false} />);
182 });
183
184 const expectedStructure = {
185 nodeName: 'svg',
186 children: [
187 {nodeName: 'defs'},
188 {
189 nodeName: 'g',
190 children: [
191 {nodeName: 'defs'},
192 {nodeName: 'path', opacity: '0.1'},
193 {nodeName: 'path', opacity: Missing},
194 {nodeName: 'g'},
195 ],
196 },
197 ],
198 };
199
200 const realNode = container.firstChild;
201 testDOMNodeStructure(realNode, expectedStructure);
202
203 await act(() => {
204 root.render(<TestComponent flipped={true} />);
205 });
206
207 const expectedNewStructure = {
208 nodeName: 'svg',
209 children: [
210 {nodeName: 'defs'},
211 {
212 nodeName: 'g',
213 children: [
214 {nodeName: 'defs'},
215 {nodeName: 'path', opacity: Missing},
216 {nodeName: 'path', opacity: '0.1'},
217 {nodeName: 'g'},
218 ],
219 },
220 ],
221 };
222
223 testDOMNodeStructure(realNode, expectedNewStructure);
224 });
225
226 it('should be able to reorder many components', async () => {
227 class Component extends React.Component {
228 render() {
229 const chars = this.props.chars.split('');
230 return (
231 <Surface>
232 {chars.map(text => (
233 <Shape key={text} title={text} />
234 ))}
235 </Surface>
236 );
237 }
238 }
239
240 // Mini multi-child stress test: lots of reorders, some adds, some removes.
241 const before = 'abcdefghijklmnopqrst';
242 const after = 'mxhpgwfralkeoivcstzy';
243
244 const root = ReactDOMClient.createRoot(container);
245 await act(() => {
246 root.render(<Component chars={before} />);
247 });
248 const realNode = container.firstChild;
249 expect(realNode.textContent).toBe(before);
250
251 await act(() => {
252 root.render(<Component chars={after} />);
253 });
254 expect(realNode.textContent).toBe(after);
255 });
256
257 it('renders composite with lifecycle inside group', async () => {
258 let mounted = false;
259
260 class CustomShape extends React.Component {
261 render() {
262 return <Shape />;
263 }
264
265 componentDidMount() {
266 mounted = true;
267 }
268 }
269 const root = ReactDOMClient.createRoot(container);
270 await act(() => {
271 root.render(
272 <Surface>
273 <Group>
274 <CustomShape />
275 </Group>
276 </Surface>,
277 );
278 });
279 expect(mounted).toBe(true);
280 });
281
282 it('resolves refs before componentDidMount', async () => {
283 class CustomShape extends React.Component {
284 render() {
285 return <Shape />;
286 }
287 }
288
289 let ref = null;
290
291 class Outer extends React.Component {
292 test = React.createRef();
293
294 componentDidMount() {
295 ref = this.test.current;
296 }
297
298 render() {
299 return (
300 <Surface>
301 <Group>
302 <CustomShape ref={this.test} />
303 </Group>
304 </Surface>
305 );
306 }
307 }
308
309 const root = ReactDOMClient.createRoot(container);
310 await act(() => {
311 root.render(<Outer />);
312 });
313 expect(ref.constructor).toBe(CustomShape);
314 });
315
316 it('resolves refs before componentDidUpdate', async () => {
317 class CustomShape extends React.Component {
318 render() {
319 return <Shape />;
320 }
321 }
322
323 let ref = {};
324
325 class Outer extends React.Component {
326 test = React.createRef();
327
328 componentDidMount() {
329 ref = this.test.current;
330 }
331
332 componentDidUpdate() {
333 ref = this.test.current;
334 }
335
336 render() {
337 return (
338 <Surface>
339 <Group>
340 {this.props.mountCustomShape && <CustomShape ref={this.test} />}
341 </Group>
342 </Surface>
343 );
344 }
345 }
346
347 const root = ReactDOMClient.createRoot(container);
348 await act(() => {
349 root.render(<Outer />);
350 });
351 expect(ref).toBe(null);
352
353 await act(() => {
354 root.render(<Outer mountCustomShape={true} />);
355 });
356 expect(ref.constructor).toBe(CustomShape);
357 });
358
359 it('adds and updates event handlers', async () => {
360 const root = ReactDOMClient.createRoot(container);
361
362 async function render(onClick) {
363 await act(() => {
364 root.render(
365 <Surface>
366 <Shape onClick={onClick} />
367 </Surface>,
368 );
369 });
370 }
371
372 function doClick(instance) {
373 const path = container.firstChild.querySelector('path');
374
375 path.dispatchEvent(
376 new MouseEvent('click', {
377 bubbles: true,
378 }),
379 );
380 }
381
382 const onClick1 = jest.fn();
383 let instance = await render(onClick1);
384 doClick(instance);
385 expect(onClick1).toHaveBeenCalled();
386
387 const onClick2 = jest.fn();
388 instance = await render(onClick2);
389 doClick(instance);
390 expect(onClick2).toHaveBeenCalled();
391 });
392 });
393
394 describe('ReactARTComponents', () => {
395 let ReactTestRenderer;
396 beforeEach(() => {
397 jest.resetModules();
398 // share isomorphic
399 jest.mock('scheduler', () => Scheduler);
400 jest.mock('react', () => React);
401 // Isolate test renderer.
402 ReactTestRenderer = require('react-test-renderer');
403 });
404
405 it('should generate a <Shape> with props for drawing the Circle', async () => {
406 let circle;
407 await act(() => {
408 circle = ReactTestRenderer.create(
409 <Circle radius={10} stroke="green" strokeWidth={3} fill="blue" />,
410 {unstable_isConcurrent: true},
411 );
412 });
413 expect(circle.toJSON()).toMatchSnapshot();
414 });
415
416 it('should generate a <Shape> with props for drawing the Rectangle', async () => {
417 let rectangle;
418 await act(() => {
419 rectangle = ReactTestRenderer.create(
420 <Rectangle width={50} height={50} stroke="green" fill="blue" />,
421 {unstable_isConcurrent: true},
422 );
423 });
424 expect(rectangle.toJSON()).toMatchSnapshot();
425 });
426
427 it('should generate a <Shape> with positive width when width prop is negative', async () => {
428 let rectangle;
429 await act(() => {
430 rectangle = ReactTestRenderer.create(
431 <Rectangle width={-50} height={50} />,
432 {unstable_isConcurrent: true},
433 );
434 });
435 expect(rectangle.toJSON()).toMatchSnapshot();
436 });
437
438 it('should generate a <Shape> with positive height when height prop is negative', async () => {
439 let rectangle;
440 await act(() => {
441 rectangle = ReactTestRenderer.create(
442 <Rectangle height={-50} width={50} />,
443 {unstable_isConcurrent: true},
444 );
445 });
446 expect(rectangle.toJSON()).toMatchSnapshot();
447 });
448
449 it('should generate a <Shape> with a radius property of 0 when top left radius prop is negative', async () => {
450 let rectangle;
451 await act(() => {
452 rectangle = ReactTestRenderer.create(
453 <Rectangle radiusTopLeft={-25} width={50} height={50} />,
454 {unstable_isConcurrent: true},
455 );
456 });
457 expect(rectangle.toJSON()).toMatchSnapshot();
458 });
459
460 it('should generate a <Shape> with a radius property of 0 when top right radius prop is negative', async () => {
461 let rectangle;
462 await act(() => {
463 rectangle = ReactTestRenderer.create(
464 <Rectangle radiusTopRight={-25} width={50} height={50} />,
465 {unstable_isConcurrent: true},
466 );
467 });
468 expect(rectangle.toJSON()).toMatchSnapshot();
469 });
470
471 it('should generate a <Shape> with a radius property of 0 when bottom right radius prop is negative', async () => {
472 let rectangle;
473 await act(() => {
474 rectangle = ReactTestRenderer.create(
475 <Rectangle radiusBottomRight={-30} width={50} height={50} />,
476 {unstable_isConcurrent: true},
477 );
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', async () => {
483 let rectangle;
484 await act(() => {
485 rectangle = ReactTestRenderer.create(
486 <Rectangle radiusBottomLeft={-25} width={50} height={50} />,
487 {unstable_isConcurrent: true},
488 );
489 });
490 expect(rectangle.toJSON()).toMatchSnapshot();
491 });
492
493 it('should generate a <Shape> where top radius is 0 if the sum of the top radius is greater than width', async () => {
494 let rectangle;
495 await act(() => {
496 rectangle = ReactTestRenderer.create(
497 <Rectangle
498 radiusTopRight={25}
499 radiusTopLeft={26}
500 width={50}
501 height={40}
502 />,
503 {unstable_isConcurrent: true},
504 );
505 });
506 expect(rectangle.toJSON()).toMatchSnapshot();
507 });
508
509 it('should generate a <Shape> with props for drawing the Wedge', async () => {
510 let wedge;
511 await act(() => {
512 wedge = ReactTestRenderer.create(
513 <Wedge outerRadius={50} startAngle={0} endAngle={360} fill="blue" />,
514 {unstable_isConcurrent: true},
515 );
516 });
517 expect(wedge.toJSON()).toMatchSnapshot();
518 });
519
520 it('should return null if startAngle equals to endAngle on Wedge', async () => {
521 let wedge;
522 await act(() => {
523 wedge = ReactTestRenderer.create(
524 <Wedge outerRadius={50} startAngle={0} endAngle={0} fill="blue" />,
525 {unstable_isConcurrent: true},
526 );
527 });
528 expect(wedge.toJSON()).toBeNull();
529 });
530 });