From 1a16eb46950e1ad07ae7aa3412d973084ff2e9c2 Mon Sep 17 00:00:00 2001 From: Mitchell Lee Date: Wed, 19 Oct 2022 09:49:15 -0500 Subject: [PATCH] fix(SceneHierarchyPerformance): SceneHierarchy Tree Performance (#283) - Reduces the number of rerenders significantly in the SceneHierarchy Tree view whenever actions like activating, selecting and removing happen - Removes redundent On2 loop when computing children for a scene hierarchy node - Cleans up some new responsibilities for TreeItem and Tree Label that were added in the wrong places. Co-authored-by: Mitchell Lee --- packages/scene-composer/package.json | 8 +- .../SceneHierarchyDataProvider.tsx | 158 ++++++------- .../SceneHierarchyTree/ComponentTypeIcon.tsx | 25 +++ .../SceneHierarchyTreeItem.tsx | 77 +++---- .../SceneHierarchyTree/SceneNodeLabel.tsx | 62 +++--- .../__tests__/SceneHierarchyTreeItem.spec.tsx | 74 +++---- .../__tests__/SceneNodeLabel.specs.tsx | 102 +++++++++ .../SceneHierarchyTreeItem.spec.tsx.snap | 209 +++--------------- .../SceneNodeLabel.specs.tsx.snap | 51 +++++ .../model/ISceneHierarchyNode.ts | 1 + 10 files changed, 368 insertions(+), 399 deletions(-) create mode 100644 packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/ComponentTypeIcon.tsx create mode 100644 packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneNodeLabel.specs.tsx create mode 100644 packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneNodeLabel.specs.tsx.snap diff --git a/packages/scene-composer/package.json b/packages/scene-composer/package.json index 6a7bd1ca7..6209a6b9e 100644 --- a/packages/scene-composer/package.json +++ b/packages/scene-composer/package.json @@ -154,10 +154,10 @@ "jest": { "coverageThreshold": { "global": { - "lines": 77.40, - "statements": 76.52, - "functions": 77.02, - "branches": 63.51, + "lines": 77.47, + "statements": 76.6, + "functions": 77.57, + "branches": 63.58, "branchesTrue": 100 } } diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/SceneHierarchyDataProvider.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/SceneHierarchyDataProvider.tsx index 404ad6ffe..6b5f87b5e 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/SceneHierarchyDataProvider.tsx +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/SceneHierarchyDataProvider.tsx @@ -1,11 +1,11 @@ -import React, { FC, createContext, useContext, useCallback, useState, useEffect } from 'react'; +import React, { FC, createContext, useContext, useCallback, useState } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import useLogger from '../../../logger/react-logger/hooks/useLogger'; import { useSceneComposerId } from '../../../common/sceneComposerIdContext'; -import { ISceneNodeInternal, useStore } from '../../../store'; import { isEnvironmentNode } from '../../../utils/nodeUtils'; +import { ISceneNodeInternal, useNodeErrorState, useStore } from '../../../store'; +import useLifecycleLogging from '../../../logger/react-logger/hooks/useLifecycleLogging'; import ISceneHierarchyNode from './model/ISceneHierarchyNode'; @@ -16,7 +16,7 @@ interface ISceneHierarchyContext { searchTerms: string; selected?: string; selectionMode: SelectionMode; - getChildNodes(parentRef: string): Promise; + getChildNodes(parentRef: string): ISceneHierarchyNode[]; search(terms: string): void; select(objectRef: string): void; show(objectRef: string): void; @@ -26,6 +26,8 @@ interface ISceneHierarchyContext { move(objectRef: string, newParentRef?: string); remove(objectRef: string); getObject3DBySceneNodeRef(objectRef: string); + isViewing(): boolean; + validationErrors: { [nodeRef: string]: string }; } interface SceneHierarchyDataProviderProps { @@ -36,6 +38,7 @@ export const Context = createContext({ rootNodes: [], searchTerms: '', selectionMode: 'single', + validationErrors: {}, search: () => {}, select: () => {}, move: () => {}, @@ -45,47 +48,32 @@ export const Context = createContext({ unselect: () => {}, remove: () => {}, getObject3DBySceneNodeRef: () => {}, - async getChildNodes() { - return Promise.resolve([] as ISceneHierarchyNode[]); - }, + getChildNodes: () => [], + isViewing: () => true, }); export const useSceneHierarchyData = () => { return useContext(Context); }; -const toSceneHeirarchyNode = ( - { ref, name, parentRef, components }: ISceneNodeInternal | Readonly, - canExpand: boolean, -) => { +const toSceneHeirarchyNode = ({ + ref, + name, + parentRef, + childRefs = [], + components, +}: ISceneNodeInternal | Readonly) => { return { objectRef: ref, name, componentTypes: components.map((c) => c.type), - hasChildren: canExpand, + childRefs, parentRef, } as ISceneHierarchyNode; }; -export const useChildNodes = (parentRef: string) => { - const { getChildNodes } = useSceneHierarchyData(); - const [loading, setLoading] = useState(false); - const [childNodes, setChildNodes] = useState([] as ISceneHierarchyNode[]); - - useEffect(() => { - (async () => { - setLoading(true); - const results = await getChildNodes(parentRef); - setChildNodes(results); - setLoading(false); - })(); - }, [getChildNodes]); - - return [childNodes, loading] as [ISceneHierarchyNode[], boolean]; -}; - const searchMatcher = (node: ISceneNodeInternal, terms: string) => { - return node.name.indexOf(terms) >= 0; // Basic search matching algorithm; + return node.name.toLowerCase().includes(terms.toLowerCase()); // Basic search matching algorithm; }; const sortNodes = (a, b) => { @@ -95,126 +83,117 @@ const sortNodes = (a, b) => { }; const SceneHierarchyDataProvider: FC = ({ selectionMode, children }) => { - const log = useLogger('SceneHierarchyDataProvider'); - + useLifecycleLogging('SceneHierarchyDataProvider'); const sceneComposerId = useSceneComposerId(); - const { - document, - selectedSceneNodeRef, - getSceneNodeByRef, - setSelectedSceneNodeRef, - updateSceneNodeInternal, - getObject3DBySceneNodeRef, - setCameraTarget, - removeSceneNode, - isEditing, - } = useStore(sceneComposerId)((state) => state); - - const { nodeMap } = document; + const selectedSceneNodeRef = useStore(sceneComposerId)((state) => state.selectedSceneNodeRef); + const getSceneNodeByRef = useStore(sceneComposerId)((state) => state.getSceneNodeByRef); + const getObject3DBySceneNodeRef = useStore(sceneComposerId)((state) => state.getObject3DBySceneNodeRef); + const isViewing = useStore(sceneComposerId)((state) => state.isViewing); + + const { nodeErrorMap: validationErrors } = useNodeErrorState(sceneComposerId); + + const unfilteredNodeMap = useStore(sceneComposerId)((state) => state.document.nodeMap); + + const [searchTerms, setSearchTerms] = useState(''); + + const nodeMap = + searchTerms === '' + ? unfilteredNodeMap + : Object.values(unfilteredNodeMap).filter((node) => searchMatcher(node, searchTerms)); const rootNodeRefs = Object.values(nodeMap) - .filter((item) => !item.parentRef && (!isEnvironmentNode(item) || isEditing())) + .filter((item) => !item.parentRef && (!isEnvironmentNode(item) || !isViewing())) .map((item) => item.ref); - const [searchTerms, setSearchTerms] = useState(''); - const [filteredNodeMap, setFilteredNodeMap] = useState([] as ISceneNodeInternal[]); - - useEffect(() => { - if (searchTerms === '') { - setFilteredNodeMap([]); - } else { - const matchingNodes = Object.values(nodeMap).filter((node) => searchMatcher(node, searchTerms)); - setFilteredNodeMap(matchingNodes); - } - }, [nodeMap, searchTerms]); - - const rootNodes: Readonly[] = - filteredNodeMap.length > 0 - ? filteredNodeMap - : rootNodeRefs - .map(getSceneNodeByRef) - .filter((node) => node !== undefined && searchMatcher(node, searchTerms)) - .map((item) => item as ISceneNodeInternal) - .sort(sortNodes); + const rootNodes: Readonly[] = rootNodeRefs + .map(getSceneNodeByRef) + .filter((node) => node !== undefined && searchMatcher(node, searchTerms)) + .map((item) => item as ISceneNodeInternal) + .sort(sortNodes); const getChildNodes = useCallback( - async (parentRef?: string) => { + (parentRef?: string) => { + const nodeMap = useStore(sceneComposerId).getState().document.nodeMap; const results = Object.values(nodeMap) .filter((node) => node.parentRef === parentRef) - .map((item) => - toSceneHeirarchyNode(item, Object.values(nodeMap).filter((n) => n.parentRef === item.ref).length > 0), - ) + .map(toSceneHeirarchyNode) .sort(sortNodes); - return Promise.resolve(results); + return results; }, - [getSceneNodeByRef, sceneComposerId, nodeMap, rootNodeRefs, log], + [sceneComposerId], ); const activate = useCallback( (nodeRef: string) => { + const setCameraTarget = useStore(sceneComposerId).getState().setCameraTarget; setCameraTarget(nodeRef, 'transition'); }, - [setCameraTarget], + [sceneComposerId], ); - const search = useCallback( - (terms: string) => { - setSearchTerms(terms); - }, - [nodeMap], - ); + const search = useCallback((terms: string) => { + setSearchTerms(terms); + }, []); const select = useCallback( - (objectRef: string) => { - setSelectedSceneNodeRef(objectRef); + (objectRef?: string) => { + if (sceneComposerId) { + const setSelectedSceneNodeRef = useStore(sceneComposerId).getState().setSelectedSceneNodeRef; + setSelectedSceneNodeRef(objectRef); + } }, - [selectedSceneNodeRef, selectionMode], + [sceneComposerId], ); const unselect = useCallback(() => { - setSelectedSceneNodeRef(undefined); // TODO: Our existing state machine doesn't consider the possibility of multi-select - }, [selectedSceneNodeRef]); + select(undefined); // TODO: Our existing state machine doesn't consider the possibility of multi-select + }, []); const move = useCallback( (objectRef: string, newParentRef?: string) => { + const updateSceneNodeInternal = useStore(sceneComposerId).getState().updateSceneNodeInternal; updateSceneNodeInternal(objectRef, { parentRef: newParentRef }); }, - [updateSceneNodeInternal, getSceneNodeByRef, nodeMap], + [sceneComposerId], ); const show = useCallback( (objectRef: string) => { + const getObject3DBySceneNodeRef = useStore(sceneComposerId).getState().getObject3DBySceneNodeRef; const object = getObject3DBySceneNodeRef(objectRef); if (object) { object.visible = true; } }, - [getObject3DBySceneNodeRef], + [sceneComposerId], ); const hide = useCallback( (objectRef: string) => { + const getObject3DBySceneNodeRef = useStore(sceneComposerId).getState().getObject3DBySceneNodeRef; const object = getObject3DBySceneNodeRef(objectRef); if (object) { object.visible = false; } }, - [getObject3DBySceneNodeRef], + [sceneComposerId], ); const remove = useCallback( (objectRef: string) => { + const removeSceneNode = useStore(sceneComposerId).getState().removeSceneNode; removeSceneNode(objectRef); }, - [removeSceneNode], + [sceneComposerId], ); return ( toSceneHeirarchyNode(item, item.childRefs.length > 0)), + rootNodes: rootNodes.map(toSceneHeirarchyNode), + validationErrors, activate, selected: selectedSceneNodeRef, move, @@ -228,6 +207,7 @@ const SceneHierarchyDataProvider: FC = ({ selec remove, getChildNodes, getObject3DBySceneNodeRef, + isViewing, }} > {children} diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/ComponentTypeIcon.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/ComponentTypeIcon.tsx new file mode 100644 index 000000000..4beb6946c --- /dev/null +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/ComponentTypeIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Icon } from '@awsui/components-react'; + +import { Camera, Light, Modelref, Tag } from '../../../../../assets/auto-gen/icons'; +import { KnownComponentType } from '../../../../../interfaces'; + +const ComponentTypeIcon = ({ type, ...props }: { type: string }) => { + switch (type) { + case KnownComponentType.Camera: + return } />; + case KnownComponentType.Light: + return } />; + case KnownComponentType.ModelRef: + case KnownComponentType.SubModelRef: + return } />; + case KnownComponentType.Tag: + return } />; + default: + return <>; + } +}; + +ComponentTypeIcon.displayName = ComponentTypeIcon; + +export default ComponentTypeIcon; diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneHierarchyTreeItem.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneHierarchyTreeItem.tsx index 63662ac0d..960841288 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneHierarchyTreeItem.tsx +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneHierarchyTreeItem.tsx @@ -1,13 +1,13 @@ -import React, { FC, useCallback, useContext, useState } from 'react'; +import React, { FC, useCallback, useState } from 'react'; import { Object3D } from 'three'; import ISceneHierarchyNode from '../../model/ISceneHierarchyNode'; -import { useChildNodes, useSceneHierarchyData } from '../../SceneHierarchyDataProvider'; +import { useSceneHierarchyData } from '../../SceneHierarchyDataProvider'; import { DropHandler } from '../../../../../hooks/useDropMonitor'; import SubModelTree from '../SubModelTree'; -import { useNodeErrorState, useStore } from '../../../../../store'; -import { sceneComposerIdContext, useSceneComposerId } from '../../../../../common/sceneComposerIdContext'; -import { isEnvironmentNode } from '../../../../../utils/nodeUtils'; +import { KnownComponentType } from '../../../../../interfaces'; +import { IModelRefComponentInternal } from '../../../../../store'; +import { ModelType } from '../../../../../models/SceneModels'; import SceneNodeLabel from './SceneNodeLabel'; import { AcceptableDropTypes, EnhancedTree, EnhancedTreeItem } from './constants'; @@ -22,22 +22,32 @@ const SceneHierarchyTreeItem: FC = ({ name: labelText, componentTypes, enableDragAndDrop, + childRefs = [], expanded: defaultExpanded = true, }: SceneHierarchyTreeItemProps) => { const [expanded, setExpanded] = useState(defaultExpanded); - const [visible, setVisible] = useState(true); - const [childNodes] = useChildNodes(key); - const { selected, select, unselect, activate, move, show, hide, remove, selectionMode, getObject3DBySceneNodeRef } = - useSceneHierarchyData(); - const { nodeErrorMap } = useNodeErrorState(useContext(sceneComposerIdContext)); + const { + selected, + select, + unselect, + getChildNodes, + activate, + move, + selectionMode, + getObject3DBySceneNodeRef, + isViewing, + } = useSceneHierarchyData(); const model = getObject3DBySceneNodeRef(key) as Object3D | undefined; - const sceneComposerId = useSceneComposerId(); - const isViewing = useStore(sceneComposerId)((state) => state.isViewing); - const node = useStore(sceneComposerId)((state) => state.getSceneNodeByRef(key)); - const showSubModel = !isEnvironmentNode(node) && !!model && !isViewing(); + const isValidModelRef = componentTypes?.find( + (type) => + type === KnownComponentType.ModelRef && + (type as unknown as IModelRefComponentInternal)?.modelType !== ModelType.Environment, + ); + + const showSubModel = isValidModelRef && !!model && !isViewing(); const onExpandNode = useCallback((expanded) => { setExpanded(expanded); @@ -64,39 +74,13 @@ const SceneHierarchyTreeItem: FC = ({ [key], ); - const onVisibilityChange = useCallback( - (newVisibility) => { - if (newVisibility) { - show(key); - } else { - hide(key); - } - - setVisible(newVisibility); - }, - [key, visible, show, hide], - ); - - const onDelete = useCallback(() => { - remove(key); - }, [key]); - return ( - } + labelText={} onExpand={onExpandNode} expanded={expanded} - expandable={childNodes.length > 0} + expandable={childRefs.length > 0} selected={selected === key} selectionMode={selectionMode} onSelected={isViewing() ? onActivated : onToggle} @@ -107,11 +91,12 @@ const SceneHierarchyTreeItem: FC = ({ dataType={componentTypes && componentTypes.length > 0 ? componentTypes[0] : /* istanbul ignore next */ 'default'} // TODO: This is somewhat based on the current assumption that items will currently only really have one componentType data={{ ref: key }} > - {childNodes && expanded && ( + {expanded && ( - {childNodes.map((node) => ( - - ))} + {childRefs.length > 0 && + getChildNodes(key).map((node) => ( + + ))} {showSubModel && } )} diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneNodeLabel.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneNodeLabel.tsx index 4434d2244..ee7ce2a38 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneNodeLabel.tsx +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/SceneNodeLabel.tsx @@ -1,55 +1,47 @@ -import React, { FC, useCallback } from 'react'; +import React, { FC, useCallback, useState } from 'react'; import { Button, Icon } from '@awsui/components-react'; import VisibilityToggle from '../../../../../components/VisibilityToggle'; import { KnownComponentType } from '../../../../../interfaces'; -import { Camera, Light, Modelref, Tag } from '../../../../../assets/auto-gen/icons'; import './SceneNodeLabel.scss'; import { DeleteSvg } from '../../../../../assets/svgs'; +import { useSceneHierarchyData } from '../../SceneHierarchyDataProvider'; + +import ComponentTypeIcon from './ComponentTypeIcon'; -const ComponentTypeIcon = ({ type, ...props }: { type: string }) => { - switch (type) { - case KnownComponentType.Camera: - return } />; - case KnownComponentType.Light: - return } />; - case KnownComponentType.ModelRef: - case KnownComponentType.SubModelRef: - return } />; - case KnownComponentType.Tag: - return } />; - default: - return <>; - } -}; interface SceneNodeLabelProps { + objectRef: string; labelText: string; componentTypes?: string[]; - error?: string; - visible?: boolean; - onVisibilityChange?: (newVisibility: boolean) => void; - onDelete: () => void; } -const SceneNodeLabel: FC = ({ - labelText, - componentTypes, - error, - visible, - onVisibilityChange = () => {}, - onDelete, -}) => { - const toggleVisibility = useCallback( - (show: boolean) => { - onVisibilityChange(show); - }, - [onVisibilityChange], - ); +const SceneNodeLabel: FC = ({ objectRef, labelText, componentTypes }) => { + const { show, hide, remove, validationErrors } = useSceneHierarchyData(); + const [visible, setVisible] = useState(true); + + const error = validationErrors[objectRef]; const componentTypeIcons = componentTypes ?.filter((type) => !!type && Object.keys(KnownComponentType).includes(type)) .map((type) => ); + const toggleVisibility = useCallback( + (newVisibility) => { + if (newVisibility) { + show(objectRef); + } else { + hide(objectRef); + } + + setVisible(newVisibility); + }, + [objectRef, visible, show, hide], + ); + + const onDelete = useCallback(() => { + remove(objectRef); + }, [objectRef]); + return ( {componentTypeIcons} diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneHierarchyTreeItem.spec.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneHierarchyTreeItem.spec.tsx index 05feef680..df2b4c753 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneHierarchyTreeItem.spec.tsx +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneHierarchyTreeItem.spec.tsx @@ -1,13 +1,19 @@ import React, { useCallback } from 'react'; import { render } from '@testing-library/react'; -import { useSceneHierarchyData, useChildNodes } from '../../../SceneHierarchyDataProvider'; +import { useSceneHierarchyData } from '../../../SceneHierarchyDataProvider'; import SceneHierarchyTreeItem from '../SceneHierarchyTreeItem'; +import { KnownComponentType } from '../../../../../../interfaces'; jest.mock('../../../../../../enhancers/draggable', () => (item: any) => item); jest.mock('../../../../../../enhancers/droppable', () => (item: any) => item); jest.mock('../../../SceneHierarchyDataProvider'); jest.mock('../../SubModelTree', () => (props) =>
); +jest.mock('../constants', () => ({ + EnhancedTree: 'EnhancedTree', + EnhancedTreeItem: 'EnhancedTreeItem', + AcceptableDropTypes: 'AcceptableDropTypes', +})); jest.mock('react', () => ({ ...jest.requireActual('react'), @@ -19,10 +25,9 @@ describe('SceneHierarchyTreeItem', () => { const unselect = jest.fn(); const activate = jest.fn(); const move = jest.fn(); - const show = jest.fn(); - const hide = jest.fn(); const getObject3DBySceneNodeRef = jest.fn(); - const remove = jest.fn(); + const getChildNodes = jest.fn(); + const isViewing = jest.fn(); let callbacks: any[] = []; beforeEach(() => { @@ -37,14 +42,11 @@ describe('SceneHierarchyTreeItem', () => { move, getObject3DBySceneNodeRef, selectionMode: 'single', - show, - hide, - remove, + getChildNodes, + isViewing, }; }); - (useChildNodes as unknown as jest.Mock).mockImplementation(() => [[]]); - (useCallback as jest.Mock).mockImplementation((cb) => callbacks.push(cb)); }); @@ -53,7 +55,7 @@ describe('SceneHierarchyTreeItem', () => { }); it('should unselect when toggled off', () => { - render(); + render(); const [, onToggle] = callbacks; @@ -64,7 +66,7 @@ describe('SceneHierarchyTreeItem', () => { }); it('should select when toggled on', () => { - render(); + render(); const [, onToggle] = callbacks; @@ -75,8 +77,16 @@ describe('SceneHierarchyTreeItem', () => { }); it('should bubble up when expanded', () => { + getChildNodes.mockImplementationOnce((key) => [{ name: key }]); + const { container } = render( - , + , ); const [onExpandNode] = callbacks; @@ -86,7 +96,7 @@ describe('SceneHierarchyTreeItem', () => { }); it('should activate and select on activation', () => { - render(); + render(); const [, , onActivated] = callbacks; onActivated(); @@ -96,7 +106,7 @@ describe('SceneHierarchyTreeItem', () => { }); it('should reparent item when dropped', () => { - render(); + render(); const [, , , dropHandler] = callbacks; @@ -105,33 +115,6 @@ describe('SceneHierarchyTreeItem', () => { expect(move).toBeCalledWith('droppedItem', '1'); }); - it('should toggle visibility when visibility is changed', () => { - render(); - - const [, , , , onVisibilityChange] = callbacks; - onVisibilityChange(true); - - expect(show).toBeCalled(); - expect(hide).not.toBeCalled(); - - show.mockReset(); - hide.mockReset(); - - onVisibilityChange(false); - - expect(show).not.toBeCalled(); - expect(hide).toBeCalled(); - }); - - it('should remove item from scene when deleted', () => { - render(); - - const [, , , , , onDelete] = callbacks; - onDelete(); - - expect(remove).toBeCalledWith('1'); - }); - it('should render SubModelTree when item has a model, and not in view mode', () => { const mockGetObject3D = getObject3DBySceneNodeRef as jest.Mock; @@ -139,8 +122,15 @@ describe('SceneHierarchyTreeItem', () => { getObjectByName: jest.fn(() => ({ scene: 'scene' })), })); + isViewing.mockImplementationOnce(() => false); + const { container } = render( - , + , ); expect(container).toMatchSnapshot(); diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneNodeLabel.specs.tsx b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneNodeLabel.specs.tsx new file mode 100644 index 000000000..0896ed401 --- /dev/null +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/SceneNodeLabel.specs.tsx @@ -0,0 +1,102 @@ +import React, { useCallback } from 'react'; +import { render } from '@testing-library/react'; + +import SceneNodeLabel from '../SceneNodeLabel'; +import { useSceneHierarchyData } from '../../../SceneHierarchyDataProvider'; +import { KnownComponentType } from '../../../../../../interfaces'; + +jest.mock('../../../../../../components/VisibilityToggle', () => 'VisibilityToggle'); +jest.mock('../../../../../../assets/svgs', () => ({ + DeleteSvg: 'DeleteSVG', +})); + +jest.mock('../ComponentTypeIcon', () => 'ComponentTypeIcon'); + +jest.mock('../../../SceneHierarchyDataProvider'); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useCallback: jest.fn(), +})); + +describe('SceneNodeLabel', () => { + const show = jest.fn(); + const hide = jest.fn(); + const remove = jest.fn(); + const validationErrors = jest.fn(); + let callbacks: any[] = []; + + beforeEach(() => { + callbacks = []; + + (useSceneHierarchyData as unknown as jest.Mock).mockImplementation(() => { + return { + show, + hide, + remove, + validationErrors, + }; + }); + + (useCallback as jest.Mock).mockImplementation((cb) => callbacks.push(cb)); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`should render with no errors`, () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it(`should allow deleting node if there's errors`, () => { + const objectRef = 'Batman'; + + (useSceneHierarchyData as unknown as jest.Mock).mockImplementation(() => { + return { + validationErrors: { [objectRef]: 'There is an error' }, + }; + }); + + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should toggle visibility', () => { + const objectRef = 'Batman'; + render(); + + const [toggleVisibility] = callbacks; + + toggleVisibility(true); + + expect(show).toBeCalledWith(objectRef); + + toggleVisibility(false); + + expect(hide).toBeCalledWith(objectRef); + }); + + it('should delete node when delete button is clicked', () => { + const objectRef = 'Batman'; + + (useSceneHierarchyData as unknown as jest.Mock).mockImplementation(() => { + return { + validationErrors: { [objectRef]: 'There is an error' }, + remove, + }; + }); + + render(); + + const [, onDelete] = callbacks; + + onDelete(); + + expect(remove).toBeCalledWith(objectRef); + }); +}); diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneHierarchyTreeItem.spec.tsx.snap b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneHierarchyTreeItem.spec.tsx.snap index 803dd61d3..a8033d41c 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneHierarchyTreeItem.spec.tsx.snap +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneHierarchyTreeItem.spec.tsx.snap @@ -2,207 +2,50 @@ exports[`SceneHierarchyTreeItem should bubble up when expanded 1`] = `
-
  • -
    -
    - -

    - Label 1 -

    - -
    -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
      - + + + +
  • `; exports[`SceneHierarchyTreeItem should render SubModelTree when item has a model, and not in view mode 1`] = `
    -
  • -
    -
    - -
    -
    - - - - - - - - - - - -
    -
    -

    - Label 1 -

    - -
    -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
      -
    -
  • + +
    `; diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneNodeLabel.specs.tsx.snap b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneNodeLabel.specs.tsx.snap new file mode 100644 index 000000000..a49c4ef5d --- /dev/null +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/components/SceneHierarchyTree/__tests__/__snapshots__/SceneNodeLabel.specs.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SceneNodeLabel should allow deleting node if there's errors 1`] = ` +
    + + +

    + Text +

    + +
    + + + +
    +`; + +exports[`SceneNodeLabel should render with no errors 1`] = ` +
    + + +

    + Text +

    + + + +
    +
    +`; diff --git a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/model/ISceneHierarchyNode.ts b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/model/ISceneHierarchyNode.ts index 5ade70aed..dd4818ee3 100644 --- a/packages/scene-composer/src/components/panels/SceneHierarchyPanel/model/ISceneHierarchyNode.ts +++ b/packages/scene-composer/src/components/panels/SceneHierarchyPanel/model/ISceneHierarchyNode.ts @@ -1,6 +1,7 @@ interface ISceneHierarchyNode { objectRef: string; name: string; + childRefs: string[]; componentTypes?: string[]; parentRef?: string; }