From 3c19fe611c57d271592f2d118c1bf97bf0e02ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Rol=C3=B3n?= <37310205+Angatupyry@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:47:43 -0300 Subject: [PATCH] Test for react three fiber component (#816) * Separate components to make it more testable Signed-off-by: angatupyry * Create react three components tests Signed-off-by: angatupyry * Add test for shape three rendering component Signed-off-by: angatupyry * Create three fiber component test Signed-off-by: angatupyry * Add test to layers controller Signed-off-by: angatupyry * Add test id to the component Signed-off-by: angatupyry * Install test-renderer in order to create test for three. Remove datatest id because it make app break Signed-off-by: angatupyry * Fix spanish description Signed-off-by: angatupyry * Change the way to import circle shape component Signed-off-by: angatupyry * Fixing merge conflict Signed-off-by: angatupyry --------- Signed-off-by: angatupyry --- .../three-fiber/layer-control.test.tsx | 386 ++++++++++++++++++ .../three-fiber/layers-controller.tsx | 6 +- .../lib/map/three-fiber/circle-shape.spec.tsx | 42 ++ .../lib/map/three-fiber/circle-shape.tsx | 44 ++ .../lib/map/three-fiber/cube-marker.spec.tsx | 27 ++ .../lib/map/three-fiber/index.tsx | 1 + .../three-fiber/robot-three-maker.spec.tsx | 29 ++ .../lib/map/three-fiber/robot-three-maker.tsx | 47 +-- .../shape-three-rendering.spec.tsx | 30 ++ .../lib/map/three-fiber/text-maker.spec.tsx | 11 + packages/react-components/package.json | 1 + pnpm-lock.yaml | 15 + 12 files changed, 593 insertions(+), 46 deletions(-) create mode 100644 packages/dashboard/src/components/three-fiber/layer-control.test.tsx create mode 100644 packages/react-components/lib/map/three-fiber/circle-shape.spec.tsx create mode 100644 packages/react-components/lib/map/three-fiber/circle-shape.tsx create mode 100644 packages/react-components/lib/map/three-fiber/cube-marker.spec.tsx create mode 100644 packages/react-components/lib/map/three-fiber/robot-three-maker.spec.tsx create mode 100644 packages/react-components/lib/map/three-fiber/shape-three-rendering.spec.tsx create mode 100644 packages/react-components/lib/map/three-fiber/text-maker.spec.tsx diff --git a/packages/dashboard/src/components/three-fiber/layer-control.test.tsx b/packages/dashboard/src/components/three-fiber/layer-control.test.tsx new file mode 100644 index 000000000..3dceffdcf --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/layer-control.test.tsx @@ -0,0 +1,386 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { LayersController } from './layers-controller'; + +describe('LayersController', () => { + it('renders the component and handles interactions correctly', () => { + const levels = [ + { + name: 'L1', + elevation: 0, + images: [ + { + name: 'office', + x_offset: 0, + y_offset: 0, + yaw: 0, + scale: 0.008465494960546494, + encoding: 'png', + data: '/assets/office.png', + }, + ], + places: [], + doors: [ + { + name: 'main_door', + v1_x: 12.18443775177002, + v1_y: -2.59969425201416, + v2_x: 14.079463958740234, + v2_y: -2.5592799186706543, + door_type: 6, + motion_range: 1.5707963705062866, + motion_direction: 1, + }, + { + name: 'coe_door', + v1_x: 8.256338119506836, + v1_y: -5.49263334274292, + v2_x: 7.8990349769592285, + v2_y: -6.304050922393799, + door_type: 5, + motion_range: 1.5707963705062866, + motion_direction: 1, + }, + { + name: 'hardware_door', + v1_x: 19.447721481323242, + v1_y: -10.76378345489502, + v2_x: 19.452404022216797, + v2_y: -9.874534606933594, + door_type: 5, + motion_range: 1.5707963705062866, + motion_direction: 1, + }, + ], + nav_graphs: [ + { + name: '0', + vertices: [ + { + x: 6.897922515869141, + y: -2.025369644165039, + name: '', + params: [ + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { x: 10.247854232788086, y: -3.0920557975769043, name: '', params: [] }, + { x: 16.855825424194336, y: -6.881363868713379, name: '', params: [] }, + { + x: 18.739524841308594, + y: -6.873981952667236, + name: '', + params: [ + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { + x: 16.84633445739746, + y: -5.404067039489746, + name: 'pantry', + params: [ + { + name: 'pickup_dispenser', + type: 1, + value_int: 0, + value_float: 0, + value_string: 'coke_dispenser', + value_bool: false, + }, + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { x: 13.130566596984863, y: -3.964806079864502, name: '', params: [] }, + { x: 8.91185474395752, y: -6.181352138519287, name: '', params: [] }, + { x: 10.086146354675293, y: -6.97943639755249, name: '', params: [] }, + { + x: 7.914645195007324, + y: -7.918869495391846, + name: '', + params: [ + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { x: 18.8138427734375, y: -11.0789794921875, name: '', params: [] }, + { x: 18.794422149658203, y: -10.372406959533691, name: '', params: [] }, + { x: 7.9883036613464355, y: -10.780257225036621, name: '', params: [] }, + { x: 9.376501083374023, y: -11.142885208129883, name: '', params: [] }, + { + x: 6.264034271240234, + y: -3.515686273574829, + name: 'supplies', + params: [ + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + ], + }, + { x: 18.729019165039062, y: -3.895981550216675, name: '', params: [] }, + { + x: 19.89569854736328, + y: -3.4071500301361084, + name: 'lounge', + params: [ + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { + x: 10.433053970336914, + y: -5.5750956535339355, + name: 'tinyRobot1_charger', + params: [ + { + name: 'is_charger', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + ], + }, + { x: 11.565889358520508, y: -6.99680757522583, name: '', params: [] }, + { x: 15.298604965209961, y: -6.92883825302124, name: '', params: [] }, + { x: 11.55336856842041, y: -11.315971374511719, name: '', params: [] }, + { x: 11.573761940002441, y: -9.250288963317871, name: '', params: [] }, + { x: 15.15718936920166, y: -11.227091789245605, name: '', params: [] }, + { x: 15.166704177856445, y: -9.242974281311035, name: '', params: [] }, + { x: 6.517305374145508, y: -5.23292875289917, name: '', params: [] }, + { + x: 5.346484661102295, + y: -4.976813793182373, + name: 'coe', + params: [ + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { x: 17.067495346069336, y: -11.097883224487305, name: '', params: [] }, + { x: 20.893653869628906, y: -10.30837345123291, name: '', params: [] }, + { + x: 20.9482479095459, + y: -7.497346878051758, + name: 'hardware_2', + params: [ + { + name: 'dropoff_ingestor', + type: 1, + value_int: 0, + value_float: 0, + value_string: 'coke_ingestor', + value_bool: false, + }, + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: false, + }, + ], + }, + { + x: 20.42369270324707, + y: -5.312098026275635, + name: 'tinyRobot2_charger', + params: [ + { + name: 'is_charger', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_holding_point', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + { + name: 'is_parking_spot', + type: 4, + value_int: 0, + value_float: 0, + value_string: '', + value_bool: true, + }, + ], + }, + ], + edges: [ + { v1_idx: 0, v2_idx: 1, params: [], edge_type: 0 }, + { v1_idx: 2, v2_idx: 3, params: [], edge_type: 0 }, + { v1_idx: 2, v2_idx: 4, params: [], edge_type: 0 }, + { v1_idx: 5, v2_idx: 1, params: [], edge_type: 0 }, + { v1_idx: 1, v2_idx: 6, params: [], edge_type: 0 }, + { v1_idx: 6, v2_idx: 7, params: [], edge_type: 0 }, + { v1_idx: 6, v2_idx: 8, params: [], edge_type: 0 }, + { v1_idx: 9, v2_idx: 10, params: [], edge_type: 0 }, + { v1_idx: 8, v2_idx: 11, params: [], edge_type: 0 }, + { v1_idx: 11, v2_idx: 12, params: [], edge_type: 0 }, + { v1_idx: 0, v2_idx: 13, params: [], edge_type: 0 }, + { v1_idx: 14, v2_idx: 5, params: [], edge_type: 0 }, + { v1_idx: 14, v2_idx: 15, params: [], edge_type: 0 }, + { v1_idx: 7, v2_idx: 16, params: [], edge_type: 0 }, + { v1_idx: 7, v2_idx: 17, params: [], edge_type: 0 }, + { v1_idx: 17, v2_idx: 18, params: [], edge_type: 0 }, + { v1_idx: 18, v2_idx: 2, params: [], edge_type: 0 }, + { v1_idx: 19, v2_idx: 20, params: [], edge_type: 0 }, + { v1_idx: 20, v2_idx: 17, params: [], edge_type: 0 }, + { v1_idx: 21, v2_idx: 22, params: [], edge_type: 0 }, + { v1_idx: 22, v2_idx: 18, params: [], edge_type: 0 }, + { v1_idx: 6, v2_idx: 23, params: [], edge_type: 0 }, + { v1_idx: 23, v2_idx: 24, params: [], edge_type: 0 }, + { v1_idx: 3, v2_idx: 10, params: [], edge_type: 0 }, + { v1_idx: 9, v2_idx: 25, params: [], edge_type: 0 }, + { v1_idx: 10, v2_idx: 26, params: [], edge_type: 0 }, + { v1_idx: 26, v2_idx: 27, params: [], edge_type: 0 }, + { v1_idx: 12, v2_idx: 19, params: [], edge_type: 0 }, + { v1_idx: 19, v2_idx: 21, params: [], edge_type: 0 }, + { v1_idx: 21, v2_idx: 25, params: [], edge_type: 0 }, + { v1_idx: 14, v2_idx: 28, params: [], edge_type: 0 }, + { v1_idx: 14, v2_idx: 3, params: [], edge_type: 0 }, + ], + params: [], + }, + ], + wall_graph: { name: '', vertices: [], edges: [], params: [] }, + }, + ]; + + const onChangeMock = jest.fn(); + const handleZoomInMock = jest.fn(); + const handleZoomOutMock = jest.fn(); + + const disabledLayers = { + Layer1: false, + Layer2: true, + Layer3: false, + }; + + const { getByText, getByLabelText, getByTestId } = render( + , + ); + + expect(getByLabelText('Levels')).toBeTruthy(); + + fireEvent.click(getByTestId('zoom-in')); + expect(handleZoomInMock).toHaveBeenCalled(); + + fireEvent.click(getByTestId('zoom-out')); + + expect(handleZoomOutMock).toHaveBeenCalled(); + + fireEvent.mouseEnter(getByTestId('layers')); + + expect(getByText('Layer1')).toBeTruthy(); + expect(getByText('Layer2')).toBeTruthy(); + expect(getByText('Layer3')).toBeTruthy(); + }); +}); diff --git a/packages/dashboard/src/components/three-fiber/layers-controller.tsx b/packages/dashboard/src/components/three-fiber/layers-controller.tsx index f54ba05e3..2e0ab1e01 100644 --- a/packages/dashboard/src/components/three-fiber/layers-controller.tsx +++ b/packages/dashboard/src/components/three-fiber/layers-controller.tsx @@ -68,17 +68,17 @@ export const LayersController = ({
- +
- +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> - + {isHovered && ( diff --git a/packages/react-components/lib/map/three-fiber/circle-shape.spec.tsx b/packages/react-components/lib/map/three-fiber/circle-shape.spec.tsx new file mode 100644 index 000000000..47b92eeba --- /dev/null +++ b/packages/react-components/lib/map/three-fiber/circle-shape.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { CircleShape } from './circle-shape'; +import { Euler, Vector3 } from 'three'; +import { makeRobotData } from '../test-utils.spec'; +import { Canvas } from '@react-three/fiber'; + +describe('CircleShape', () => { + it('should render a circle and a line correctly', () => { + const position = new Vector3(1, 1, 0); + const rotation = new Euler(0, 0, Math.PI / 4); + const onRobotClick = jasmine.createSpy(); + const robot = makeRobotData({ + name: 'test_robot_1', + inConflict: false, + }); + const segment = 32; + + const { container } = render( + + + , + ); + + const circle = container.querySelector('circle'); + expect(circle).toBeDefined(); + + const line = container.querySelector('line'); + expect(line).toBeDefined(); + + if (circle) { + circle.dispatchEvent(new MouseEvent('click')); + expect(onRobotClick).toHaveBeenCalled(); + } + }); +}); diff --git a/packages/react-components/lib/map/three-fiber/circle-shape.tsx b/packages/react-components/lib/map/three-fiber/circle-shape.tsx new file mode 100644 index 000000000..042b5d834 --- /dev/null +++ b/packages/react-components/lib/map/three-fiber/circle-shape.tsx @@ -0,0 +1,44 @@ +import { Circle, Line } from '@react-three/drei'; +import { ThreeEvent } from '@react-three/fiber'; +import React from 'react'; +import { Euler, Vector3 } from 'three'; +import { RobotData } from './robot-three-maker'; + +interface CircleShapeProps { + position: Vector3; + rotation: Euler; + onRobotClick?: (ev: ThreeEvent) => void; + robot: RobotData; + segment: number; +} + +export const CircleShape = ({ + position, + rotation, + onRobotClick, + robot, + segment, +}: CircleShapeProps): JSX.Element => { + const SCALED_RADIUS = 0.7; + + const rotatedX = position.x + SCALED_RADIUS * Math.cos(rotation.z - Math.PI / 2); + const rotatedY = position.y + SCALED_RADIUS * Math.sin(rotation.z - Math.PI / 2); + + return ( + <> + + + + + + ); +}; diff --git a/packages/react-components/lib/map/three-fiber/cube-marker.spec.tsx b/packages/react-components/lib/map/three-fiber/cube-marker.spec.tsx new file mode 100644 index 000000000..064f8ef4f --- /dev/null +++ b/packages/react-components/lib/map/three-fiber/cube-marker.spec.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { CubeMaker } from './cube-maker'; +import { Euler } from 'three'; + +describe('CubeMaker', () => { + it('should render a cube with the provided properties.', () => { + const position = [0, 0, 0]; + const size = [1, 1, 1]; + const rot = new Euler(0, 0, 0); + const color = 'red'; + + const { container } = render( + , + ); + + const cubeElement = container.querySelector('mesh'); + + expect(cubeElement).toBeTruthy(); + + expect(cubeElement?.getAttribute('position')).toBe('0,0,0'); + expect(cubeElement?.getAttribute('scale')).toBe('1,1,1'); + + const materialElement = cubeElement?.querySelector('meshStandardMaterial'); + expect(materialElement?.getAttribute('color')).toBe('red'); + }); +}); diff --git a/packages/react-components/lib/map/three-fiber/index.tsx b/packages/react-components/lib/map/three-fiber/index.tsx index c8383f4af..5b226b5a6 100644 --- a/packages/react-components/lib/map/three-fiber/index.tsx +++ b/packages/react-components/lib/map/three-fiber/index.tsx @@ -6,3 +6,4 @@ export * from './robot-three-maker'; export * from './shape-three-rendering'; export * from './image-maker'; export * from './text-maker'; +export * from './circle-shape'; diff --git a/packages/react-components/lib/map/three-fiber/robot-three-maker.spec.tsx b/packages/react-components/lib/map/three-fiber/robot-three-maker.spec.tsx new file mode 100644 index 000000000..d8835827e --- /dev/null +++ b/packages/react-components/lib/map/three-fiber/robot-three-maker.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { RobotThreeMaker } from './robot-three-maker'; +import { Euler, Vector3 } from 'three'; +import ReactThreeTestRenderer from '@react-three/test-renderer'; + +describe('RobotThreeMaker', () => { + it('renders color properly', async () => { + const robot = { + fleet: 'Fleet 1', + name: 'Robot 1', + model: 'Model 1', + footprint: 1.0, + color: 'blue', + }; + + const renderer = await ReactThreeTestRenderer.create( + , + ); + + const mesh = renderer.scene.children[0].allChildren; + + expect(mesh.length).toBe(1); + }); +}); diff --git a/packages/react-components/lib/map/three-fiber/robot-three-maker.tsx b/packages/react-components/lib/map/three-fiber/robot-three-maker.tsx index 870fd21b0..bb7e1c0b0 100644 --- a/packages/react-components/lib/map/three-fiber/robot-three-maker.tsx +++ b/packages/react-components/lib/map/three-fiber/robot-three-maker.tsx @@ -1,8 +1,8 @@ -import { Circle, Line } from '@react-three/drei'; import { ThreeEvent, useLoader } from '@react-three/fiber'; import React from 'react'; -import { Euler, Vector3, Texture, TextureLoader, Color } from 'three'; -import { TextThreeRendering } from '.'; +import { Color, Euler, Texture, TextureLoader, Vector3 } from 'three'; +import { CircleShape } from './circle-shape'; +import { TextThreeRendering } from './text-maker'; export interface RobotData { fleet: string; @@ -32,45 +32,6 @@ interface RobotImageMakerProps { rotation: Euler; } -interface CircleShapeProps { - position: Vector3; - rotation: Euler; - onRobotClick?: (ev: ThreeEvent) => void; - robot: RobotData; - segment: number; -} - -const CircleShape = ({ - position, - rotation, - onRobotClick, - robot, - segment, -}: CircleShapeProps): JSX.Element => { - const SCALED_RADIUS = 0.7; - - const rotatedX = position.x + SCALED_RADIUS * Math.cos(rotation.z - Math.PI / 2); - const rotatedY = position.y + SCALED_RADIUS * Math.sin(rotation.z - Math.PI / 2); - - return ( - <> - - - - - - ); -}; - const RobotImageMaker = ({ imageUrl, position, @@ -121,7 +82,7 @@ export const RobotThreeMaker = ({ }: RobotThreeMakerProps): JSX.Element => { return ( <> - + {imageUrl ? ( { + it('renders correctly with non-circle shape', async () => { + const renderer = await ReactThreeTestRenderer.create( + , + ); + + const mesh = renderer.scene.children[0].allChildren; + + expect(mesh.length).toBe(1); + }); + + it('renders color properly', async () => { + const renderer = await ReactThreeTestRenderer.create( + , + ); + + const searchByColor = renderer.scene.findAll((node) => node.props.color === 'blue'); + + expect(searchByColor.length).toBe(1); + }); +}); diff --git a/packages/react-components/lib/map/three-fiber/text-maker.spec.tsx b/packages/react-components/lib/map/three-fiber/text-maker.spec.tsx new file mode 100644 index 000000000..6f523e632 --- /dev/null +++ b/packages/react-components/lib/map/three-fiber/text-maker.spec.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { TextThreeRendering } from './text-maker'; + +describe('TextThreeRendering', () => { + it('should handle the absence of text correctly', () => { + const { container } = render(); + const meshElements = container.querySelectorAll('mesh'); + expect(meshElements.length).toBe(2); + }); +}); diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 6c0d719a0..cf4a8898d 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -28,6 +28,7 @@ "@mui/x-date-pickers": "^5.0.20", "@react-three/drei": "^9.84.0", "@react-three/fiber": "^8.14.2", + "@react-three/test-renderer": "^8.2.0", "@types/crc": "^3.4.0", "@types/rbush": "^3.0.0", "@types/react-grid-layout": "^1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e1595d3c..487a01129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -360,6 +360,9 @@ importers: '@react-three/fiber': specifier: ^8.14.2 version: 8.14.2(react-dom@18.2.0)(react@18.2.0)(three@0.156.1) + '@react-three/test-renderer': + specifier: ^8.2.0 + version: 8.2.0(@react-three/fiber@8.14.2)(react@18.2.0)(three@0.156.1) '@types/crc': specifier: ^3.4.0 version: 3.8.0 @@ -3889,6 +3892,18 @@ packages: zustand: 3.7.2(react@18.2.0) dev: false + /@react-three/test-renderer@8.2.0(@react-three/fiber@8.14.2)(react@18.2.0)(three@0.156.1): + resolution: {integrity: sha512-sYTW/9AkU0f03M/rilYaCB9ORD3tS96bUhM+WRsx/QLtOKdUNCWrWwnxutm5M0orON0A0O84gbUosnqvCAKTsw==} + peerDependencies: + '@react-three/fiber': '>=8.0.0' + react: '>=17.0' + three: '>=0.126' + dependencies: + '@react-three/fiber': 8.14.2(react-dom@18.2.0)(react@18.2.0)(three@0.156.1) + react: 18.2.0 + three: 0.156.1 + dev: false + /@remix-run/router@1.7.1: resolution: {integrity: sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==} engines: {node: '>=14'}