mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-09 08:38:13 +00:00
Compare commits
3 Commits
update-ing
...
codex/veri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e01ff5bc4 | ||
|
|
12b3cc8fca | ||
|
|
427e9a20c4 |
48
codecov.yml
48
codecov.yml
@@ -5,6 +5,54 @@ comment:
|
||||
require_base: false
|
||||
require_head: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
critical-unit:
|
||||
target: auto
|
||||
threshold: 0.15%
|
||||
flags:
|
||||
- unit
|
||||
paths:
|
||||
- src/base/
|
||||
- src/composables/
|
||||
- src/core/
|
||||
- src/lib/litegraph/src/node/
|
||||
- src/lib/litegraph/src/subgraph/
|
||||
- src/lib/litegraph/src/utils/
|
||||
- src/platform/assets/composables/
|
||||
- src/platform/assets/mappings/
|
||||
- src/platform/assets/schemas/
|
||||
- src/platform/assets/services/
|
||||
- src/platform/assets/utils/
|
||||
- src/platform/errorCatalog/
|
||||
- src/platform/keybindings/
|
||||
- src/platform/missingMedia/
|
||||
- src/platform/missingModel/
|
||||
- src/platform/navigation/
|
||||
- src/platform/nodeReplacement/
|
||||
- src/platform/remote/
|
||||
- src/platform/remoteConfig/
|
||||
- src/platform/secrets/
|
||||
- src/platform/settings/
|
||||
- src/platform/workflow/
|
||||
- src/platform/workspace/api/
|
||||
- src/platform/workspace/auth/
|
||||
- src/platform/workspace/composables/
|
||||
- src/platform/workspace/stores/
|
||||
- src/platform/workspace/utils/
|
||||
- src/schemas/
|
||||
- src/scripts/
|
||||
- src/services/
|
||||
- src/stores/
|
||||
- src/utils/
|
||||
- src/workbench/extensions/manager/composables/
|
||||
- src/workbench/extensions/manager/services/
|
||||
- src/workbench/extensions/manager/stores/
|
||||
- src/workbench/extensions/manager/utils/
|
||||
- src/workbench/utils/
|
||||
informational: false
|
||||
|
||||
# Carry forward the last known coverage for a flag when its upload is missing or
|
||||
# late. The `e2e` flag is uploaded by a separate workflow_run job that can fail
|
||||
# or arrive after Codecov has already computed the patch status; without this,
|
||||
|
||||
@@ -12,6 +12,11 @@ export type {
|
||||
AddAssetTagsErrors,
|
||||
AddAssetTagsResponse,
|
||||
AddAssetTagsResponses,
|
||||
AdminDeleteHubWorkflowData,
|
||||
AdminDeleteHubWorkflowError,
|
||||
AdminDeleteHubWorkflowErrors,
|
||||
AdminDeleteHubWorkflowResponse,
|
||||
AdminDeleteHubWorkflowResponses,
|
||||
Asset,
|
||||
AssetCreated,
|
||||
AssetCreatedWritable,
|
||||
@@ -84,6 +89,11 @@ export type {
|
||||
CreateAssetFromHashResponses,
|
||||
CreateAssetResponse,
|
||||
CreateAssetResponses,
|
||||
CreateDeletionRequestData,
|
||||
CreateDeletionRequestError,
|
||||
CreateDeletionRequestErrors,
|
||||
CreateDeletionRequestResponse,
|
||||
CreateDeletionRequestResponses,
|
||||
CreateDesktopLoginCodeData,
|
||||
CreateDesktopLoginCodeError,
|
||||
CreateDesktopLoginCodeErrors,
|
||||
@@ -160,6 +170,9 @@ export type {
|
||||
DeleteHubWorkflowErrors,
|
||||
DeleteHubWorkflowResponse,
|
||||
DeleteHubWorkflowResponses,
|
||||
DeleteMonitoringTasksSubpathData,
|
||||
DeleteMonitoringTasksSubpathErrors,
|
||||
DeleteMonitoringTasksSubpathResponses,
|
||||
DeleteSecretData,
|
||||
DeleteSecretError,
|
||||
DeleteSecretErrors,
|
||||
@@ -186,6 +199,8 @@ export type {
|
||||
DeleteWorkspaceErrors,
|
||||
DeleteWorkspaceResponse,
|
||||
DeleteWorkspaceResponses,
|
||||
DeletionRequest,
|
||||
DeletionStatus,
|
||||
DesktopLoginCodeCreateRequest,
|
||||
DesktopLoginCodeCreateResponse,
|
||||
DesktopLoginCodeExchangeRequest,
|
||||
@@ -197,6 +212,14 @@ export type {
|
||||
DownloadExportErrors,
|
||||
DownloadExportResponse,
|
||||
DownloadExportResponses,
|
||||
EnsureWorkspaceBillingLegacySnapshot,
|
||||
EnsureWorkspaceBillingProvisionedData,
|
||||
EnsureWorkspaceBillingProvisionedError,
|
||||
EnsureWorkspaceBillingProvisionedErrors,
|
||||
EnsureWorkspaceBillingProvisionedRequest,
|
||||
EnsureWorkspaceBillingProvisionedResponse,
|
||||
EnsureWorkspaceBillingProvisionedResponse2,
|
||||
EnsureWorkspaceBillingProvisionedResponses,
|
||||
ErrorResponse,
|
||||
ExchangeDesktopLoginCodeData,
|
||||
ExchangeDesktopLoginCodeError,
|
||||
@@ -219,6 +242,7 @@ export type {
|
||||
ExportDownloadUrlResponse,
|
||||
FeedbackRequest,
|
||||
FeedbackResponse,
|
||||
FeedbackResponseWritable,
|
||||
ForkWorkflowData,
|
||||
ForkWorkflowError,
|
||||
ForkWorkflowErrors,
|
||||
@@ -281,6 +305,11 @@ export type {
|
||||
GetCustomNodeProxyData,
|
||||
GetCustomNodeProxyErrors,
|
||||
GetCustomNodeProxyResponses,
|
||||
GetDeletionRequestData,
|
||||
GetDeletionRequestError,
|
||||
GetDeletionRequestErrors,
|
||||
GetDeletionRequestResponse,
|
||||
GetDeletionRequestResponses,
|
||||
GetEmbeddingsData,
|
||||
GetEmbeddingsResponse,
|
||||
GetEmbeddingsResponses,
|
||||
@@ -290,6 +319,11 @@ export type {
|
||||
GetFeaturesData,
|
||||
GetFeaturesResponse,
|
||||
GetFeaturesResponses,
|
||||
GetFilesData,
|
||||
GetFilesError,
|
||||
GetFilesErrors,
|
||||
GetFilesResponse,
|
||||
GetFilesResponses,
|
||||
GetGlobalSubgraphData,
|
||||
GetGlobalSubgraphError,
|
||||
GetGlobalSubgraphErrors,
|
||||
@@ -308,6 +342,11 @@ export type {
|
||||
GetHistoryData,
|
||||
GetHistoryError,
|
||||
GetHistoryErrors,
|
||||
GetHistoryEventsData,
|
||||
GetHistoryEventsError,
|
||||
GetHistoryEventsErrors,
|
||||
GetHistoryEventsResponse,
|
||||
GetHistoryEventsResponses,
|
||||
GetHistoryForPromptData,
|
||||
GetHistoryForPromptError,
|
||||
GetHistoryForPromptErrors,
|
||||
@@ -370,6 +409,11 @@ export type {
|
||||
GetLegacyUserdataV2Errors,
|
||||
GetLegacyViewMetadataData,
|
||||
GetLegacyViewMetadataErrors,
|
||||
GetLogsData,
|
||||
GetLogsError,
|
||||
GetLogsErrors,
|
||||
GetLogsResponse,
|
||||
GetLogsResponses,
|
||||
GetMaskLayersData,
|
||||
GetMaskLayersError,
|
||||
GetMaskLayersErrors,
|
||||
@@ -387,6 +431,12 @@ export type {
|
||||
GetModelsInFolderErrors,
|
||||
GetModelsInFolderResponse,
|
||||
GetModelsInFolderResponses,
|
||||
GetMonitoringTasksData,
|
||||
GetMonitoringTasksErrors,
|
||||
GetMonitoringTasksResponses,
|
||||
GetMonitoringTasksSubpathData,
|
||||
GetMonitoringTasksSubpathErrors,
|
||||
GetMonitoringTasksSubpathResponses,
|
||||
GetMyHubProfileData,
|
||||
GetMyHubProfileError,
|
||||
GetMyHubProfileErrors,
|
||||
@@ -430,6 +480,12 @@ export type {
|
||||
GetPaymentPortalErrors,
|
||||
GetPaymentPortalResponse,
|
||||
GetPaymentPortalResponses,
|
||||
GetPprofData,
|
||||
GetPprofProfileData,
|
||||
GetPprofProfileResponses,
|
||||
GetPprofResponses,
|
||||
GetPprofTraceData,
|
||||
GetPprofTraceResponses,
|
||||
GetPromptInfoData,
|
||||
GetPromptInfoError,
|
||||
GetPromptInfoErrors,
|
||||
@@ -542,6 +598,7 @@ export type {
|
||||
HistoryDetailEntry,
|
||||
HistoryDetailResponse,
|
||||
HistoryEntry,
|
||||
HistoryEventRequest,
|
||||
HistoryManageRequest,
|
||||
HistoryResponse,
|
||||
HubAssetUploadUrlRequest,
|
||||
@@ -563,6 +620,11 @@ export type {
|
||||
ImportPublishedAssetsResponse,
|
||||
ImportPublishedAssetsResponse2,
|
||||
ImportPublishedAssetsResponses,
|
||||
InsertDynamicConfigData,
|
||||
InsertDynamicConfigError,
|
||||
InsertDynamicConfigErrors,
|
||||
InsertDynamicConfigResponse,
|
||||
InsertDynamicConfigResponses,
|
||||
InterruptJobData,
|
||||
InterruptJobError,
|
||||
InterruptJobErrors,
|
||||
@@ -610,6 +672,13 @@ export type {
|
||||
ListJobsErrors,
|
||||
ListJobsResponse,
|
||||
ListJobsResponses,
|
||||
ListLinkedFirebaseUidsData,
|
||||
ListLinkedFirebaseUidsError,
|
||||
ListLinkedFirebaseUidsErrors,
|
||||
ListLinkedFirebaseUidsRequest,
|
||||
ListLinkedFirebaseUidsResponse,
|
||||
ListLinkedFirebaseUidsResponse2,
|
||||
ListLinkedFirebaseUidsResponses,
|
||||
ListMembersResponse,
|
||||
ListSecretProvidersData,
|
||||
ListSecretProvidersError,
|
||||
@@ -659,6 +728,7 @@ export type {
|
||||
ListWorkspacesResponse,
|
||||
ListWorkspacesResponse2,
|
||||
ListWorkspacesResponses,
|
||||
LogsResponse,
|
||||
ManageHistoryData,
|
||||
ManageHistoryError,
|
||||
ManageHistoryErrors,
|
||||
@@ -689,6 +759,8 @@ export type {
|
||||
OAuthTokenError,
|
||||
OAuthTokenResponse,
|
||||
PaginationInfo,
|
||||
PartnerUsageRequest,
|
||||
PartnerUsageResponse,
|
||||
PaymentPortalRequest,
|
||||
PaymentPortalResponse,
|
||||
PendingInvite,
|
||||
@@ -704,6 +776,9 @@ export type {
|
||||
PostCustomNodeProxyData,
|
||||
PostCustomNodeProxyErrors,
|
||||
PostCustomNodeProxyResponses,
|
||||
PostMonitoringTasksSubpathData,
|
||||
PostMonitoringTasksSubpathErrors,
|
||||
PostMonitoringTasksSubpathResponses,
|
||||
PostOAuthAuthorizeData,
|
||||
PostOAuthAuthorizeError,
|
||||
PostOAuthAuthorizeErrors,
|
||||
@@ -719,6 +794,8 @@ export type {
|
||||
PostOAuthTokenErrors,
|
||||
PostOAuthTokenResponse,
|
||||
PostOAuthTokenResponses,
|
||||
PostPprofSymbolData,
|
||||
PostPprofSymbolResponses,
|
||||
PostUserdataFileData,
|
||||
PostUserdataFileError,
|
||||
PostUserdataFileErrors,
|
||||
@@ -760,6 +837,12 @@ export type {
|
||||
RedeemDesktopLoginCodeErrors,
|
||||
RedeemDesktopLoginCodeResponse,
|
||||
RedeemDesktopLoginCodeResponses,
|
||||
ReleaseDeletionHoldData,
|
||||
ReleaseDeletionHoldError,
|
||||
ReleaseDeletionHoldErrors,
|
||||
ReleaseDeletionHoldResponse,
|
||||
ReleaseDeletionHoldResponses,
|
||||
ReleaseHoldResponse,
|
||||
RemoveAssetTagsData,
|
||||
RemoveAssetTagsError,
|
||||
RemoveAssetTagsErrors,
|
||||
@@ -770,6 +853,16 @@ export type {
|
||||
RemoveWorkspaceMemberErrors,
|
||||
RemoveWorkspaceMemberResponse,
|
||||
RemoveWorkspaceMemberResponses,
|
||||
ReportHistoryEventData,
|
||||
ReportHistoryEventError,
|
||||
ReportHistoryEventErrors,
|
||||
ReportHistoryEventResponse,
|
||||
ReportHistoryEventResponses,
|
||||
ReportPartnerUsageData,
|
||||
ReportPartnerUsageError,
|
||||
ReportPartnerUsageErrors,
|
||||
ReportPartnerUsageResponse,
|
||||
ReportPartnerUsageResponses,
|
||||
ResubscribeData,
|
||||
ResubscribeError,
|
||||
ResubscribeErrors,
|
||||
@@ -794,6 +887,15 @@ export type {
|
||||
SeedAssetsData,
|
||||
SeedAssetsResponse,
|
||||
SeedAssetsResponses,
|
||||
SetReviewStatusData,
|
||||
SetReviewStatusError,
|
||||
SetReviewStatusErrors,
|
||||
SetReviewStatusRequest,
|
||||
SetReviewStatusResponse,
|
||||
SetReviewStatusResponse2,
|
||||
SetReviewStatusResponses,
|
||||
ShortLinkRedirectData,
|
||||
ShortLinkRedirectErrors,
|
||||
SubmitFeedbackData,
|
||||
SubmitFeedbackError,
|
||||
SubmitFeedbackErrors,
|
||||
@@ -810,6 +912,13 @@ export type {
|
||||
SubscribeToLogsResponses,
|
||||
SubscriptionDuration,
|
||||
SubscriptionTier,
|
||||
SyncApiKeyData,
|
||||
SyncApiKeyError,
|
||||
SyncApiKeyErrors,
|
||||
SyncApiKeyRequest,
|
||||
SyncApiKeyResponse,
|
||||
SyncApiKeyResponse2,
|
||||
SyncApiKeyResponses,
|
||||
SystemStatsResponse,
|
||||
TagInfo,
|
||||
TagsModificationResponse,
|
||||
@@ -831,6 +940,12 @@ export type {
|
||||
UpdateHubProfileRequest,
|
||||
UpdateHubProfileResponse,
|
||||
UpdateHubProfileResponses,
|
||||
UpdateHubWorkflowData,
|
||||
UpdateHubWorkflowError,
|
||||
UpdateHubWorkflowErrors,
|
||||
UpdateHubWorkflowRequest,
|
||||
UpdateHubWorkflowResponse,
|
||||
UpdateHubWorkflowResponses,
|
||||
UpdateMemberRoleRequest,
|
||||
UpdateMultipleSettingsData,
|
||||
UpdateMultipleSettingsError,
|
||||
@@ -848,6 +963,11 @@ export type {
|
||||
UpdateSettingByIdErrors,
|
||||
UpdateSettingByIdResponse,
|
||||
UpdateSettingByIdResponses,
|
||||
UpdateSubscriptionCacheData,
|
||||
UpdateSubscriptionCacheError,
|
||||
UpdateSubscriptionCacheErrors,
|
||||
UpdateSubscriptionCacheResponse,
|
||||
UpdateSubscriptionCacheResponses,
|
||||
UpdateWorkflowData,
|
||||
UpdateWorkflowError,
|
||||
UpdateWorkflowErrors,
|
||||
@@ -879,6 +999,13 @@ export type {
|
||||
UserResponse,
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
VerifyApiKeyRequest,
|
||||
VerifyApiKeyResponse,
|
||||
VerifyWorkspaceApiKeyData,
|
||||
VerifyWorkspaceApiKeyError,
|
||||
VerifyWorkspaceApiKeyErrors,
|
||||
VerifyWorkspaceApiKeyResponse,
|
||||
VerifyWorkspaceApiKeyResponses,
|
||||
ViewFileData,
|
||||
ViewFileError,
|
||||
ViewFileErrors,
|
||||
|
||||
15870
packages/ingest-types/src/types.gen.ts
generated
15870
packages/ingest-types/src/types.gen.ts
generated
File diff suppressed because it is too large
Load Diff
6710
packages/ingest-types/src/zod.gen.ts
generated
6710
packages/ingest-types/src/zod.gen.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -1,743 +0,0 @@
|
||||
import * as THREE from 'three'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
const { nodeMap, useLoad3dViewerMock, skeletonCloneMock } = vi.hoisted(() => ({
|
||||
nodeMap: new Map<LGraphNode, Load3d>(),
|
||||
useLoad3dViewerMock: vi.fn(),
|
||||
skeletonCloneMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useLoad3d', () => ({
|
||||
nodeToLoad3dMap: nodeMap
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useLoad3dViewer', () => ({
|
||||
useLoad3dViewer: useLoad3dViewerMock
|
||||
}))
|
||||
|
||||
vi.mock('three/examples/jsm/utils/SkeletonUtils', () => ({
|
||||
clone: skeletonCloneMock
|
||||
}))
|
||||
|
||||
// Track every node a test creates so the load3dService singleton's
|
||||
// internal viewerInstances map can be drained in beforeEach without
|
||||
// reaching into the module's private state.
|
||||
const createdNodes = new Set<LGraphNode>()
|
||||
|
||||
function makeNode(id: number | string): LGraphNode {
|
||||
const node = createMockLGraphNode({ id })
|
||||
createdNodes.add(node)
|
||||
return node
|
||||
}
|
||||
|
||||
function makeLoad3d(): Load3d {
|
||||
return {
|
||||
remove: vi.fn()
|
||||
} as unknown as Load3d
|
||||
}
|
||||
|
||||
function makeViewer(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
needApplyChanges: { value: false },
|
||||
applyChanges: vi.fn().mockResolvedValue(true),
|
||||
cleanup: vi.fn(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('load3dService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
nodeMap.clear()
|
||||
const svc = useLoad3dService()
|
||||
for (const node of createdNodes) svc.removeViewer(node)
|
||||
createdNodes.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('singleton', () => {
|
||||
it('returns the same instance from useLoad3dService()', () => {
|
||||
expect(useLoad3dService()).toBe(useLoad3dService())
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLoad3d (sync)', () => {
|
||||
it('returns null when the load3d module has not been loaded yet', () => {
|
||||
// Before any async accessor has been called, the cache is empty.
|
||||
// We can't easily simulate "module never loaded" because vi.mock makes
|
||||
// it eagerly available, so this test verifies the behavior via missing
|
||||
// entries instead.
|
||||
const node = makeNode('missing')
|
||||
expect(useLoad3dService().getLoad3d(node)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null after async load when the node has no entry in the map', async () => {
|
||||
const svc = useLoad3dService()
|
||||
// Trigger the async loader so the sync path has a populated cache.
|
||||
await svc.getLoad3dAsync(makeNode('anything'))
|
||||
|
||||
expect(svc.getLoad3d(makeNode('still-missing'))).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the registered Load3d instance once the map has been populated', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('a')
|
||||
const load3d = makeLoad3d()
|
||||
nodeMap.set(node, load3d)
|
||||
await svc.getLoad3dAsync(node)
|
||||
|
||||
expect(svc.getLoad3d(node)).toBe(load3d)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLoad3dAsync', () => {
|
||||
it('returns the Load3d for a registered node', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('async-a')
|
||||
const load3d = makeLoad3d()
|
||||
nodeMap.set(node, load3d)
|
||||
|
||||
await expect(svc.getLoad3dAsync(node)).resolves.toBe(load3d)
|
||||
})
|
||||
|
||||
it('returns null for an unregistered node', async () => {
|
||||
const svc = useLoad3dService()
|
||||
await expect(
|
||||
svc.getLoad3dAsync(makeNode('async-missing'))
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNodeByLoad3d', () => {
|
||||
it('finds the node owning a given Load3d instance', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('owner')
|
||||
const load3d = makeLoad3d()
|
||||
nodeMap.set(node, load3d)
|
||||
await svc.getLoad3dAsync(node)
|
||||
|
||||
expect(svc.getNodeByLoad3d(load3d)).toBe(node)
|
||||
})
|
||||
|
||||
it('returns null when the Load3d instance is not in the map', async () => {
|
||||
const svc = useLoad3dService()
|
||||
await svc.getLoad3dAsync(makeNode('warmup'))
|
||||
|
||||
expect(svc.getNodeByLoad3d(makeLoad3d())).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeLoad3d', () => {
|
||||
it('calls remove() on the instance and drops it from the map', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('to-remove')
|
||||
const load3d = makeLoad3d()
|
||||
nodeMap.set(node, load3d)
|
||||
await svc.getLoad3dAsync(node)
|
||||
|
||||
svc.removeLoad3d(node)
|
||||
|
||||
expect(load3d.remove).toHaveBeenCalled()
|
||||
expect(nodeMap.has(node)).toBe(false)
|
||||
})
|
||||
|
||||
it('is a no-op when the node has no registered Load3d', async () => {
|
||||
const svc = useLoad3dService()
|
||||
await svc.getLoad3dAsync(makeNode('warmup'))
|
||||
|
||||
expect(() => svc.removeLoad3d(makeNode('not-there'))).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear', () => {
|
||||
it('removes every registered Load3d', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const a = makeNode('a')
|
||||
const b = makeNode('b')
|
||||
const ld1 = makeLoad3d()
|
||||
const ld2 = makeLoad3d()
|
||||
nodeMap.set(a, ld1)
|
||||
nodeMap.set(b, ld2)
|
||||
await svc.getLoad3dAsync(a)
|
||||
|
||||
svc.clear()
|
||||
|
||||
expect(nodeMap.size).toBe(0)
|
||||
expect(ld1.remove).toHaveBeenCalled()
|
||||
expect(ld2.remove).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('viewer lifecycle', () => {
|
||||
it('getOrCreateViewer creates a viewer on first call and reuses it on subsequent calls', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('v1')
|
||||
const viewer = makeViewer()
|
||||
useLoad3dViewerMock.mockReturnValue(viewer)
|
||||
|
||||
const first = await svc.getOrCreateViewer(node)
|
||||
const second = await svc.getOrCreateViewer(node)
|
||||
|
||||
expect(first).toBe(viewer)
|
||||
expect(second).toBe(viewer)
|
||||
expect(useLoad3dViewerMock).toHaveBeenCalledTimes(1)
|
||||
expect(useLoad3dViewerMock).toHaveBeenCalledWith(node)
|
||||
})
|
||||
|
||||
it('getOrCreateViewerSync uses the supplied factory once and caches the result', () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('v-sync')
|
||||
const viewer = makeViewer()
|
||||
const factory = vi.fn().mockReturnValue(viewer)
|
||||
|
||||
const first = svc.getOrCreateViewerSync(
|
||||
node,
|
||||
factory as unknown as typeof useLoad3dViewerMock
|
||||
)
|
||||
const second = svc.getOrCreateViewerSync(
|
||||
node,
|
||||
factory as unknown as typeof useLoad3dViewerMock
|
||||
)
|
||||
|
||||
expect(first).toBe(viewer)
|
||||
expect(second).toBe(viewer)
|
||||
expect(factory).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('removeViewer calls cleanup and forgets the viewer', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('v2')
|
||||
const viewer = makeViewer()
|
||||
useLoad3dViewerMock.mockReturnValue(viewer)
|
||||
await svc.getOrCreateViewer(node)
|
||||
|
||||
svc.removeViewer(node)
|
||||
|
||||
expect(viewer.cleanup).toHaveBeenCalled()
|
||||
useLoad3dViewerMock.mockClear()
|
||||
const fresh = makeViewer()
|
||||
useLoad3dViewerMock.mockReturnValue(fresh)
|
||||
const result = await svc.getOrCreateViewer(node)
|
||||
expect(useLoad3dViewerMock).toHaveBeenCalledTimes(1)
|
||||
expect(result).toBe(fresh)
|
||||
})
|
||||
|
||||
it('removeViewer is safe when no viewer has been created for the node', () => {
|
||||
const svc = useLoad3dService()
|
||||
expect(() => svc.removeViewer(makeNode('never'))).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleViewerClose', () => {
|
||||
it('removes the viewer without applying changes when none are pending', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('close-clean')
|
||||
const viewer = makeViewer({ needApplyChanges: { value: false } })
|
||||
useLoad3dViewerMock.mockReturnValue(viewer)
|
||||
await svc.getOrCreateViewer(node)
|
||||
|
||||
await svc.handleViewerClose(node)
|
||||
|
||||
expect(viewer.applyChanges).not.toHaveBeenCalled()
|
||||
expect(viewer.cleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies changes and syncs the node config when changes are pending', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const syncLoad3dConfig = vi.fn()
|
||||
const node = Object.assign(makeNode('close-dirty'), {
|
||||
syncLoad3dConfig
|
||||
}) as LGraphNode
|
||||
const viewer = makeViewer({ needApplyChanges: { value: true } })
|
||||
useLoad3dViewerMock.mockReturnValue(viewer)
|
||||
await svc.getOrCreateViewer(node)
|
||||
|
||||
await svc.handleViewerClose(node)
|
||||
|
||||
expect(viewer.applyChanges).toHaveBeenCalled()
|
||||
expect(syncLoad3dConfig).toHaveBeenCalled()
|
||||
expect(viewer.cleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips syncLoad3dConfig when the node does not define it', async () => {
|
||||
const svc = useLoad3dService()
|
||||
const node = makeNode('close-no-sync')
|
||||
const viewer = makeViewer({ needApplyChanges: { value: true } })
|
||||
useLoad3dViewerMock.mockReturnValue(viewer)
|
||||
await svc.getOrCreateViewer(node)
|
||||
|
||||
await expect(svc.handleViewerClose(node)).resolves.toBeUndefined()
|
||||
expect(viewer.applyChanges).toHaveBeenCalled()
|
||||
expect(viewer.cleanup).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleViewportRefresh', () => {
|
||||
it('returns silently when the load3d is null', () => {
|
||||
expect(() => useLoad3dService().handleViewportRefresh(null)).not.toThrow()
|
||||
})
|
||||
|
||||
it('toggles the camera through the opposite type and back, then updates controls', () => {
|
||||
const controls = { update: vi.fn() }
|
||||
const load3d = {
|
||||
handleResize: vi.fn(),
|
||||
getCurrentCameraType: vi.fn().mockReturnValue('perspective'),
|
||||
toggleCamera: vi.fn(),
|
||||
getControlsManager: vi.fn().mockReturnValue({ controls })
|
||||
} as unknown as Load3d
|
||||
|
||||
useLoad3dService().handleViewportRefresh(load3d)
|
||||
|
||||
expect(load3d.handleResize).toHaveBeenCalled()
|
||||
expect(load3d.toggleCamera).toHaveBeenNthCalledWith(1, 'orthographic')
|
||||
expect(load3d.toggleCamera).toHaveBeenNthCalledWith(2, 'perspective')
|
||||
expect(controls.update).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('toggles in the reverse direction when starting from orthographic', () => {
|
||||
const controls = { update: vi.fn() }
|
||||
const load3d = {
|
||||
handleResize: vi.fn(),
|
||||
getCurrentCameraType: vi.fn().mockReturnValue('orthographic'),
|
||||
toggleCamera: vi.fn(),
|
||||
getControlsManager: vi.fn().mockReturnValue({ controls })
|
||||
} as unknown as Load3d
|
||||
|
||||
useLoad3dService().handleViewportRefresh(load3d)
|
||||
|
||||
expect(load3d.toggleCamera).toHaveBeenNthCalledWith(1, 'perspective')
|
||||
expect(load3d.toggleCamera).toHaveBeenNthCalledWith(2, 'orthographic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copyLoad3dState', () => {
|
||||
type SourceOverrides = Partial<{
|
||||
currentModel: THREE.Object3D | null
|
||||
isSplat: boolean
|
||||
originalURL: string | null
|
||||
originalModel: unknown
|
||||
materialMode: string
|
||||
currentUpDirection: string
|
||||
appliedTexture: unknown
|
||||
gizmoEnabled: boolean
|
||||
hasAnimations: boolean
|
||||
cameraType: 'perspective' | 'orthographic'
|
||||
backgroundInfo: { type: 'image' | 'color' }
|
||||
lightsIntensity: number | undefined
|
||||
fov: number
|
||||
}>
|
||||
|
||||
function makeSource(overrides: SourceOverrides = {}): Load3d {
|
||||
const {
|
||||
currentModel = null,
|
||||
isSplat = false,
|
||||
originalURL = null,
|
||||
originalModel = null,
|
||||
materialMode = 'original',
|
||||
currentUpDirection = 'original',
|
||||
appliedTexture = null,
|
||||
gizmoEnabled = false,
|
||||
hasAnimations = false,
|
||||
cameraType = 'perspective',
|
||||
backgroundInfo = { type: 'color' },
|
||||
lightsIntensity = 0.8,
|
||||
fov = 35
|
||||
} = overrides
|
||||
const ambient = { intensity: 0.5 }
|
||||
const main = { intensity: lightsIntensity }
|
||||
return {
|
||||
modelManager: { currentModel, originalURL },
|
||||
getGizmoManager: () => ({
|
||||
isEnabled: () => gizmoEnabled,
|
||||
getInitialTransform: () => ({
|
||||
position: { x: 1, y: 2, z: 3 },
|
||||
rotation: { x: 0.1, y: 0.2, z: 0.3 },
|
||||
scale: { x: 4, y: 5, z: 6 }
|
||||
})
|
||||
}),
|
||||
isSplatModel: () => isSplat,
|
||||
getModelManager: () => ({
|
||||
originalModel,
|
||||
materialMode,
|
||||
currentUpDirection,
|
||||
appliedTexture
|
||||
}),
|
||||
getGizmoTransform: () => ({
|
||||
position: { x: 7, y: 8, z: 9 },
|
||||
rotation: { x: 0.4, y: 0.5, z: 0.6 },
|
||||
scale: { x: 10, y: 11, z: 12 }
|
||||
}),
|
||||
hasAnimations: () => hasAnimations,
|
||||
getCurrentCameraType: () => cameraType,
|
||||
getCameraState: () => ({ snapshot: true }),
|
||||
getSceneManager: () => ({
|
||||
scene: new THREE.Scene(),
|
||||
currentBackgroundColor: '#abcdef',
|
||||
gridHelper: { visible: true },
|
||||
getCurrentBackgroundInfo: () => backgroundInfo
|
||||
}),
|
||||
getLightingManager: () => ({ lights: [ambient, main] }),
|
||||
getCameraManager: () => ({ perspectiveCamera: { fov } })
|
||||
} as unknown as Load3d
|
||||
}
|
||||
|
||||
type TargetState = {
|
||||
modelManager: {
|
||||
currentModel: THREE.Object3D | null
|
||||
originalModel: unknown
|
||||
materialMode: string
|
||||
currentUpDirection: string
|
||||
appliedTexture: unknown
|
||||
}
|
||||
gizmoManager: {
|
||||
isEnabled: () => boolean
|
||||
detach: ReturnType<typeof vi.fn>
|
||||
setupForModel: ReturnType<typeof vi.fn>
|
||||
}
|
||||
animationManager: {
|
||||
setupModelAnimations: ReturnType<typeof vi.fn>
|
||||
}
|
||||
sceneRemoved: THREE.Object3D[]
|
||||
sceneAdded: THREE.Object3D[]
|
||||
}
|
||||
|
||||
function makeTarget(
|
||||
opts: {
|
||||
gizmoEnabled?: boolean
|
||||
existingModel?: THREE.Object3D | null
|
||||
} = {}
|
||||
) {
|
||||
const { gizmoEnabled = false, existingModel = null } = opts
|
||||
const scene = new THREE.Scene()
|
||||
const sceneRemoved: THREE.Object3D[] = []
|
||||
const sceneAdded: THREE.Object3D[] = []
|
||||
const sceneRemove = vi.fn((o: THREE.Object3D) => {
|
||||
sceneRemoved.push(o)
|
||||
scene.remove(o)
|
||||
})
|
||||
const sceneAdd = vi.fn((o: THREE.Object3D) => {
|
||||
sceneAdded.push(o)
|
||||
scene.add(o)
|
||||
})
|
||||
const modelManager = {
|
||||
currentModel: existingModel as THREE.Object3D | null,
|
||||
originalModel: null as unknown,
|
||||
materialMode: 'original',
|
||||
currentUpDirection: 'original',
|
||||
appliedTexture: null as unknown
|
||||
}
|
||||
const animationManager = {
|
||||
setupModelAnimations: vi.fn()
|
||||
}
|
||||
// Memoize the gizmo manager so production code's repeated
|
||||
// `target.getGizmoManager()` calls reach the same vi.fn instances.
|
||||
const gizmoManager = {
|
||||
isEnabled: () => gizmoEnabled,
|
||||
detach: vi.fn(),
|
||||
setupForModel: vi.fn()
|
||||
}
|
||||
const target = {
|
||||
getGizmoManager: () => gizmoManager,
|
||||
getModelManager: () => modelManager,
|
||||
getSceneManager: () => ({
|
||||
scene: {
|
||||
add: sceneAdd,
|
||||
remove: sceneRemove
|
||||
} as unknown as THREE.Scene
|
||||
}),
|
||||
loadModel: vi.fn().mockResolvedValue(undefined),
|
||||
setMaterialMode: vi.fn(),
|
||||
setUpDirection: vi.fn(),
|
||||
applyGizmoTransform: vi.fn(),
|
||||
setGizmoEnabled: vi.fn(),
|
||||
animationManager,
|
||||
toggleCamera: vi.fn(),
|
||||
setCameraState: vi.fn(),
|
||||
setBackgroundColor: vi.fn(),
|
||||
toggleGrid: vi.fn(),
|
||||
setBackgroundImage: vi.fn().mockResolvedValue(undefined),
|
||||
setLightIntensity: vi.fn(),
|
||||
setFOV: vi.fn()
|
||||
} as unknown as Load3d
|
||||
const state: TargetState = {
|
||||
modelManager,
|
||||
gizmoManager,
|
||||
animationManager,
|
||||
sceneRemoved,
|
||||
sceneAdded
|
||||
}
|
||||
return { target, state }
|
||||
}
|
||||
|
||||
function makeModel(): THREE.Object3D {
|
||||
return new THREE.Object3D()
|
||||
}
|
||||
|
||||
it('copies camera/scene/lighting/FOV even when there is no source model', async () => {
|
||||
const source = makeSource({ currentModel: null, lightsIntensity: 2 })
|
||||
const { target } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.toggleCamera).toHaveBeenCalledWith('perspective')
|
||||
expect(target.setCameraState).toHaveBeenCalledWith({ snapshot: true })
|
||||
expect(target.setBackgroundColor).toHaveBeenCalledWith('#abcdef')
|
||||
expect(target.toggleGrid).toHaveBeenCalledWith(true)
|
||||
expect(target.setLightIntensity).toHaveBeenCalledWith(2)
|
||||
expect(target.setFOV).toHaveBeenCalledWith(35)
|
||||
expect(skeletonCloneMock).not.toHaveBeenCalled()
|
||||
expect(target.loadModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses target.loadModel(originalURL) for splat models, never invoking SkeletonUtils.clone', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
isSplat: true,
|
||||
originalURL: 'http://example.com/scan.splat'
|
||||
})
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.loadModel).toHaveBeenCalledWith(
|
||||
'http://example.com/scan.splat'
|
||||
)
|
||||
expect(skeletonCloneMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips loadModel for splat models when originalURL is null', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
isSplat: true,
|
||||
originalURL: null
|
||||
})
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.loadModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes the target existing model from the scene before adding the clone', async () => {
|
||||
const existing = makeModel()
|
||||
existing.name = 'existing'
|
||||
const source = makeSource({ currentModel: makeModel() })
|
||||
const { target, state } = makeTarget({ existingModel: existing })
|
||||
const clone = makeModel()
|
||||
skeletonCloneMock.mockReturnValue(clone)
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.sceneRemoved).toContain(existing)
|
||||
expect(state.sceneAdded).toContain(clone)
|
||||
})
|
||||
|
||||
it('clones the source model via SkeletonUtils and assigns it as the target current model', async () => {
|
||||
const sourceModel = makeModel()
|
||||
const clone = makeModel()
|
||||
const source = makeSource({ currentModel: sourceModel })
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(clone)
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(skeletonCloneMock).toHaveBeenCalledWith(sourceModel)
|
||||
expect(state.modelManager.currentModel).toBe(clone)
|
||||
})
|
||||
|
||||
it('copies originalModel, material mode, up direction, and applied texture from source to target', async () => {
|
||||
const sourceOriginal = { kind: 'gltf' }
|
||||
const texture = { id: 'tex1' }
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
originalModel: sourceOriginal,
|
||||
materialMode: 'wireframe',
|
||||
currentUpDirection: '+y',
|
||||
appliedTexture: texture
|
||||
})
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.modelManager.originalModel).toBe(sourceOriginal)
|
||||
expect(state.modelManager.materialMode).toBe('wireframe')
|
||||
expect(state.modelManager.currentUpDirection).toBe('+y')
|
||||
expect(state.modelManager.appliedTexture).toBe(texture)
|
||||
expect(target.setMaterialMode).toHaveBeenCalledWith('wireframe')
|
||||
expect(target.setUpDirection).toHaveBeenCalledWith('+y')
|
||||
})
|
||||
|
||||
it('positions the clone at the source initial transform', async () => {
|
||||
const clone = makeModel()
|
||||
const source = makeSource({ currentModel: makeModel() })
|
||||
const { target } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(clone)
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(clone.position.toArray()).toEqual([1, 2, 3])
|
||||
expect(clone.rotation.toArray().slice(0, 3)).toEqual([0.1, 0.2, 0.3])
|
||||
expect(clone.scale.toArray()).toEqual([4, 5, 6])
|
||||
})
|
||||
|
||||
it('applies the source gizmo transform to the target', async () => {
|
||||
const source = makeSource({ currentModel: makeModel() })
|
||||
const { target } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.applyGizmoTransform).toHaveBeenCalledWith(
|
||||
{ x: 7, y: 8, z: 9 },
|
||||
{ x: 0.4, y: 0.5, z: 0.6 },
|
||||
{ x: 10, y: 11, z: 12 }
|
||||
)
|
||||
})
|
||||
|
||||
it('enables the gizmo on target when the source had it enabled', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
gizmoEnabled: true
|
||||
})
|
||||
const { target } = makeTarget({ gizmoEnabled: false })
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setGizmoEnabled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('enables the gizmo on target when the target previously had it enabled, even if source did not', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
gizmoEnabled: false
|
||||
})
|
||||
const { target } = makeTarget({ gizmoEnabled: true })
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setGizmoEnabled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('does not enable the gizmo when neither side had it', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
gizmoEnabled: false
|
||||
})
|
||||
const { target } = makeTarget({ gizmoEnabled: false })
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setGizmoEnabled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forwards animation setup when the source has animations', async () => {
|
||||
const sourceOriginal = { kind: 'gltf' }
|
||||
const clone = makeModel()
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
originalModel: sourceOriginal,
|
||||
hasAnimations: true
|
||||
})
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(clone)
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.animationManager.setupModelAnimations).toHaveBeenCalledWith(
|
||||
clone,
|
||||
sourceOriginal
|
||||
)
|
||||
})
|
||||
|
||||
it('does not forward animation setup when the source has none', async () => {
|
||||
const source = makeSource({
|
||||
currentModel: makeModel(),
|
||||
hasAnimations: false
|
||||
})
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.animationManager.setupModelAnimations).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forwards an image background to setBackgroundImage when the source node has a configured path', async () => {
|
||||
const node = createMockLGraphNode({
|
||||
id: 'bg-source',
|
||||
properties: { 'Scene Config': { backgroundImage: '3d/bg.png' } }
|
||||
})
|
||||
createdNodes.add(node)
|
||||
const source = makeSource({ backgroundInfo: { type: 'image' } })
|
||||
nodeMap.set(node, source)
|
||||
// Warm the cache so `getNodeByLoad3d` finds the source.
|
||||
await useLoad3dService().getLoad3dAsync(node)
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setBackgroundImage).toHaveBeenCalledWith('3d/bg.png')
|
||||
})
|
||||
|
||||
it('clears the background when the source background type is not image', async () => {
|
||||
const source = makeSource({ backgroundInfo: { type: 'color' } })
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setBackgroundImage).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('falls back to setLightIntensity(1) when the second light intensity is falsy', async () => {
|
||||
const source = makeSource({ lightsIntensity: 0 })
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setLightIntensity).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('skips setFOV when the source camera is orthographic', async () => {
|
||||
const source = makeSource({ cameraType: 'orthographic' })
|
||||
const { target } = makeTarget()
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(target.setFOV).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('always detaches the target gizmo at the start of the copy', async () => {
|
||||
const source = makeSource({ currentModel: makeModel() })
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(makeModel())
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.gizmoManager.detach).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls setupForModel on the target gizmo with the freshly cloned model', async () => {
|
||||
const clone = makeModel()
|
||||
const source = makeSource({ currentModel: makeModel() })
|
||||
const { target, state } = makeTarget()
|
||||
skeletonCloneMock.mockReturnValue(clone)
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, target)
|
||||
|
||||
expect(state.gizmoManager.setupForModel).toHaveBeenCalledWith(clone)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user