diff --git a/react-components/tests/unit-tests/fixtures/cadModel.ts b/react-components/tests/unit-tests/fixtures/cadModel.ts index 2227193b1b6..f2a54af597f 100644 --- a/react-components/tests/unit-tests/fixtures/cadModel.ts +++ b/react-components/tests/unit-tests/fixtures/cadModel.ts @@ -1,12 +1,14 @@ import { type CogniteCadModel } from '@cognite/reveal'; -import { Mock } from 'moq.ts'; -import { Matrix4 } from 'three'; +import { It, Mock } from 'moq.ts'; +import { Box3, Matrix4, Vector3 } from 'three'; export const cadModelOptions = { modelId: 123, revisionId: 456 }; +export const nodeBoundingBox = new Box3(new Vector3(1, 1, 1), new Vector3(2, 2, 2)); + export const cadMock = new Mock() .setup((p) => p.modelId) .returns(cadModelOptions.modelId) @@ -14,4 +16,6 @@ export const cadMock = new Mock() .returns(cadModelOptions.revisionId) .setup((p) => p.getModelTransformation()) .returns(new Matrix4()) + .setup(async (p) => await p.getBoundingBoxesByNodeIds(It.IsAny())) + .returns(Promise.resolve([nodeBoundingBox])) .object(); diff --git a/react-components/tests/unit-tests/fixtures/cameraManager.ts b/react-components/tests/unit-tests/fixtures/cameraManager.ts index 7612a8c9774..58b96cf0c59 100644 --- a/react-components/tests/unit-tests/fixtures/cameraManager.ts +++ b/react-components/tests/unit-tests/fixtures/cameraManager.ts @@ -1,19 +1,20 @@ -import { CameraManager, CameraManagerEventType, CameraState } from '@cognite/reveal'; +import { type CameraManager, type CameraManagerEventType, type CameraState } from '@cognite/reveal'; import { remove } from 'lodash'; import { Mock } from 'moq.ts'; -import { Vector3 } from 'three'; +import { type Box3, type Vector3 } from 'three'; -import { vi, Mock as viMock } from 'vitest'; +import { vi, type Mock as viMock } from 'vitest'; export const cameraManagerGlobalCameraEvents: Record< CameraManagerEventType, - viMock<[Vector3, Vector3], void>[] + Array> > = { cameraChange: [], cameraStop: [] }; const cameraManagerGlobalCurrentCameraState: CameraState = {}; +export const fitCameraToBoundingBoxMock = vi.fn<[Box3], void>(); export const cameraManagerMock = new Mock() .setup((p) => p.on) @@ -31,14 +32,14 @@ export const cameraManagerMock = new Mock() .returns(({ position, target }) => { cameraManagerGlobalCurrentCameraState.position = position; cameraManagerGlobalCurrentCameraState.target = target; - setTimeout( - () => - cameraManagerGlobalCameraEvents.cameraStop.forEach((callback) => - callback(position!, target!) - ), - 50 - ); + setTimeout(() => { + cameraManagerGlobalCameraEvents.cameraStop.forEach((callback) => { + callback(position!, target!); + }); + }, 50); }) .setup((p) => p.getCameraState()) .returns(cameraManagerGlobalCurrentCameraState as Required) + .setup((p) => p.fitCameraToBoundingBox) + .returns(fitCameraToBoundingBoxMock) .object(); diff --git a/react-components/tests/unit-tests/fixtures/fdmNodeCache.ts b/react-components/tests/unit-tests/fixtures/fdmNodeCache.ts new file mode 100644 index 00000000000..fd2e4f4d5ce --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/fdmNodeCache.ts @@ -0,0 +1,68 @@ +import { Mock } from 'moq.ts'; +import { type FdmNodeCache } from '../../../src/components/CacheProvider/FdmNodeCache'; +import { type FdmNodeCacheContent } from '../../../src/components/CacheProvider/NodeCacheProvider'; +import { type DmsUniqueIdentifier } from '../../../src/data-providers/FdmSDK'; +import { type TypedReveal3DModel } from '../../../src/components/Reveal3DResources/types'; +import { type Node3D } from '@cognite/sdk'; +import { + type FdmCadConnection, + type FdmConnectionWithNode +} from '../../../src/components/CacheProvider/types'; + +const fdmNodeCacheMock = new Mock() + .setup((instance) => instance.getAllMappingExternalIds) + .returns( + async ( + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + _fetchViews: boolean + ) => { + return new Map( + modelRevisionIds.map(({ modelId, revisionId }) => [ + `${modelId}/${revisionId}`, + [ + { + connection: { + instance: { space: 'space', externalId: 'id' }, + modelId, + revisionId, + treeIndex: 1 + } satisfies FdmCadConnection, + cadNode: { + id: 1, + treeIndex: 1, + parentId: 0, + depth: 0, + name: 'node-name', + subtreeSize: 1 + } satisfies Node3D + } satisfies FdmConnectionWithNode + ] + ]) + ); + } + ) + .setup((instance) => instance.getClosestParentDataPromises) + .returns((modelId: number, revisionId: number, treeIndex: number) => { + return { + modelId, + revisionId, + treeIndex, + data: `data-for-${modelId}-${revisionId}-${treeIndex}`, + cadAndFdmNodesPromise: Promise.resolve(undefined), + viewsPromise: Promise.resolve([]) + }; + }) + .setup((instance) => instance.getMappingsForFdmInstances) + .returns(async (fdmAssetExternalIds: DmsUniqueIdentifier[], models: TypedReveal3DModel[]) => { + return models.map((model) => ({ + modelId: model.modelId, + revisionId: model.revisionId, + mappings: new Map(fdmAssetExternalIds.map((id) => [JSON.stringify(id), [] as Node3D[]])) + })); + }); + +const fdmNodeCacheContentMock: FdmNodeCacheContent = { + cache: fdmNodeCacheMock.object() +}; + +export { fdmNodeCacheContentMock }; diff --git a/react-components/tests/unit-tests/fixtures/image360.ts b/react-components/tests/unit-tests/fixtures/image360.ts index c03b2be4103..efafe8ce0a7 100644 --- a/react-components/tests/unit-tests/fixtures/image360.ts +++ b/react-components/tests/unit-tests/fixtures/image360.ts @@ -1,4 +1,4 @@ -import { Image360Collection } from '@cognite/reveal'; +import { type Image360Collection } from '@cognite/reveal'; import { Mock } from 'moq.ts'; export const image360Options = { diff --git a/react-components/tests/unit-tests/fixtures/pointCloud.ts b/react-components/tests/unit-tests/fixtures/pointCloud.ts index fa4296b36c6..9d2b8b332a5 100644 --- a/react-components/tests/unit-tests/fixtures/pointCloud.ts +++ b/react-components/tests/unit-tests/fixtures/pointCloud.ts @@ -1,4 +1,4 @@ -import { CognitePointCloudModel } from '@cognite/reveal'; +import { type CognitePointCloudModel } from '@cognite/reveal'; import { Mock } from 'moq.ts'; import { Matrix4 } from 'three'; diff --git a/react-components/tests/unit-tests/fixtures/viewer.ts b/react-components/tests/unit-tests/fixtures/viewer.ts index be8822cbd30..71cc92b10e1 100644 --- a/react-components/tests/unit-tests/fixtures/viewer.ts +++ b/react-components/tests/unit-tests/fixtures/viewer.ts @@ -9,6 +9,8 @@ const domElement = document.createElement('div').appendChild(document.createElem export const viewerModelsMock = vi.fn<[], CogniteModel[]>(); export const viewerRemoveModelsMock = vi.fn<[CogniteModel], void>(); export const viewerImage360CollectionsMock = vi.fn<[], Image360Collection[]>(); +export const fitCameraToVisualSceneBoundingBoxMock = vi.fn<[number?], void>(); +export const fitCameraToModelsMock = vi.fn<[CogniteModel[], number?, boolean?], void>(); export const viewerMock = new Mock() .setup((viewer) => { @@ -25,4 +27,8 @@ export const viewerMock = new Mock() .returns(viewerRemoveModelsMock) .setup((p) => p.cameraManager) .returns(cameraManagerMock) + .setup((p) => p.fitCameraToVisualSceneBoundingBox) + .returns(fitCameraToVisualSceneBoundingBoxMock) + .setup((p) => p.fitCameraToModels) + .returns(fitCameraToModelsMock) .object(); diff --git a/react-components/tests/unit-tests/hooks/use3dModels.test.ts b/react-components/tests/unit-tests/hooks/use3dModels.test.ts new file mode 100644 index 00000000000..3686ee1508c --- /dev/null +++ b/react-components/tests/unit-tests/hooks/use3dModels.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test, vi, beforeEach, beforeAll, afterAll } from 'vitest'; +import { use3dModels } from '../../../src/hooks/use3dModels'; +import { CogniteCadModel, type CogniteModel } from '@cognite/reveal'; +import { renderHook } from '@testing-library/react'; + +import { viewerMock, viewerModelsMock } from '../fixtures/viewer'; +import { cadMock, cadModelOptions } from '../fixtures/cadModel'; +import { Mock } from 'moq.ts'; +import { Matrix4 } from 'three'; + +const mockResourceCount = { reveal3DResourcesCount: 2 }; + +vi.mock('../../../src/components/RevealCanvas/ViewerContext', () => ({ + useReveal: () => viewerMock +})); + +vi.mock('../../../src/components/Reveal3DResources/Reveal3DResourcesInfoContext', () => ({ + useReveal3DResourcesCount: () => mockResourceCount +})); + +describe('use3dModels', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + test('returns models from viewer', () => { + const mockModels: CogniteModel[] = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + + const { result } = renderHook(() => use3dModels()); + + expect(result.current).toEqual(mockModels); + }); + + test('updates models when viewer or resourceCount changes', () => { + const mockModels: CogniteModel[] = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + + const { result, rerender } = renderHook(() => use3dModels()); + + expect(result.current).toEqual(mockModels); + + const newCadModelOptions = { + modelId: 987, + revisionId: 654 + }; + + const newCadMock = new Mock() + .setup((p) => p.modelId) + .returns(newCadModelOptions.modelId) + .setup((p) => p.revisionId) + .returns(newCadModelOptions.revisionId) + .setup((p) => p.getModelTransformation()) + .returns(new Matrix4()) + .object(); + + const newMockModels: CogniteModel[] = [newCadMock, newCadMock]; + const newMockResourceCount = { reveal3DResourcesCount: 3 }; + + viewerModelsMock.mockReturnValue(newMockModels); + mockResourceCount.reveal3DResourcesCount = newMockResourceCount.reveal3DResourcesCount; + + rerender(); + + expect(result.current).toEqual(newMockModels); + }); +}); diff --git a/react-components/tests/unit-tests/hooks/useCameraNavigation.test.ts b/react-components/tests/unit-tests/hooks/useCameraNavigation.test.ts new file mode 100644 index 00000000000..bb8b133d9ab --- /dev/null +++ b/react-components/tests/unit-tests/hooks/useCameraNavigation.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, test, vi, beforeEach, beforeAll, afterAll } from 'vitest'; +import { useCameraNavigation } from '../../../src/hooks/useCameraNavigation'; +import { viewerMock, viewerModelsMock } from '../fixtures/viewer'; +import { cadMock, nodeBoundingBox } from '../fixtures/cadModel'; +import { act, renderHook } from '@testing-library/react'; +import { fdmNodeCacheContentMock } from '../fixtures/fdmNodeCache'; +import { Vector3 } from 'three'; + +vi.mock('../../../src/components/RevealCanvas/ViewerContext', () => ({ + useReveal: () => viewerMock +})); + +vi.mock('../../../src/components/CacheProvider/NodeCacheProvider', () => ({ + useFdmNodeCache: () => fdmNodeCacheContentMock +})); + +describe('useCameraNavigation', () => { + beforeEach(() => { + vi.resetAllMocks(); + viewerMock.cameraManager.setCameraState = vi.fn(); + viewerMock.cameraManager.fitCameraToBoundingBox = vi.fn(); + }); + + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + test('fitCameraToVisualSceneBoundingBox calls viewer method', () => { + const { result } = renderHook(() => useCameraNavigation()); + + act(() => { + result.current.fitCameraToVisualSceneBoundingBox(1000); + }); + + expect(viewerMock.fitCameraToVisualSceneBoundingBox).toHaveBeenCalledWith(1000); + }); + + test('fitCameraToAllModels calls viewer method with models', () => { + const mockModels = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + + const { result } = renderHook(() => useCameraNavigation()); + + act(() => { + result.current.fitCameraToAllModels(1000); + }); + + expect(viewerMock.fitCameraToModels).toHaveBeenCalledWith(mockModels, 1000, true); + }); + + test('fitCameraToModelNodes calls viewer method with bounding box', async () => { + const mockModels = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + + const { result } = renderHook(() => useCameraNavigation()); + const fitCameraToModelNodesSpy = vi.spyOn(result.current, 'fitCameraToModelNodes'); + + await act(async () => { + await result.current.fitCameraToModelNodes(456, [1, 2]); + }); + + expect(fitCameraToModelNodesSpy).toHaveBeenCalledWith(456, [1, 2]); + + expect(viewerMock.cameraManager.fitCameraToBoundingBox).toHaveBeenCalledWith(nodeBoundingBox); + }); + + test('fitCameraToModelNode calls fitCameraToModelNodes with single node', async () => { + const mockModels = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + + const { result } = renderHook(() => useCameraNavigation()); + const fitCameraToModelNodeSpy = vi.spyOn(result.current, 'fitCameraToModelNode'); + + await act(async () => { + await result.current.fitCameraToModelNode(456, 1); + }); + + expect(fitCameraToModelNodeSpy).toHaveBeenCalledWith(456, 1); + }); + + test('fitCameraToInstances calls fitCameraToModelNodes with node ids', async () => { + const mockModels = [cadMock, cadMock]; + viewerModelsMock.mockReturnValue(mockModels); + const mockMappings = { + revisionId: 456, + mappings: new Map([['model1', [{ id: 1 }, { id: 2 }]]]) + }; + + fdmNodeCacheContentMock.cache.getMappingsForFdmInstances = vi + .fn() + .mockResolvedValue([mockMappings]); + + const { result } = renderHook(() => useCameraNavigation()); + + await act(async () => { + await result.current.fitCameraToInstances([{ externalId: 'ext1', space: 'space1' }]); + }); + + expect(viewerMock.cameraManager.fitCameraToBoundingBox).toHaveBeenCalled(); + }); + + test('fitCameraToInstance calls fitCameraToInstances with single instance', async () => { + const mockModels = [cadMock, cadMock]; + const mockMappings = { + revisionId: 456, + mappings: new Map([['model1', [{ id: 1 }]]]) + }; + viewerModelsMock.mockReturnValue(mockModels); + + fdmNodeCacheContentMock.cache.getMappingsForFdmInstances = vi + .fn() + .mockResolvedValue([mockMappings]); + + const { result } = renderHook(() => useCameraNavigation()); + + await act(async () => { + await result.current.fitCameraToInstance('ext1', 'space1'); + }); + + expect(viewerMock.cameraManager.fitCameraToBoundingBox).toHaveBeenCalled(); + }); + + test('fitCameraToState calls viewer method with camera state', () => { + const mockCameraState = { + position: new Vector3(0, 0, 0), + target: new Vector3(1, 1, 1) + }; + + const { result } = renderHook(() => useCameraNavigation()); + + act(() => { + result.current.fitCameraToState(mockCameraState); + }); + + expect(viewerMock.cameraManager.setCameraState).toHaveBeenCalledWith(mockCameraState); + }); +});