mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-10 14:29:59 +00:00
Compare commits
3 Commits
main
...
test/previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0986db81a | ||
|
|
ea3c289a07 | ||
|
|
fa7477b8c4 |
88
browser_tests/assets/3d/preview3d_pipeline.json
Normal file
88
browser_tests/assets/3d/preview3d_pipeline.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Load3D",
|
||||
"pos": [50, 50],
|
||||
"size": [400, 650],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "mesh_path",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "normal",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "camera_info",
|
||||
"type": "LOAD3D_CAMERA",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "recording_video",
|
||||
"type": "VIDEO",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "model_3d",
|
||||
"type": "FILE_3D",
|
||||
"links": [1]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Load3D"
|
||||
},
|
||||
"widgets_values": ["", 1024, 1024, "#000000"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Preview3D",
|
||||
"pos": [520, 50],
|
||||
"size": [450, 600],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model_file",
|
||||
"type": "FILE_3D",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "Preview3D"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [[1, 1, 6, 2, 0, "*"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
121
browser_tests/fixtures/helpers/Preview3DPipelineFixture.ts
Normal file
121
browser_tests/fixtures/helpers/Preview3DPipelineFixture.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { Load3DHelper } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
export async function waitForAppBootstrapped(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<void> {
|
||||
await comfyPage.page.waitForFunction(() =>
|
||||
Boolean(window.app?.extensionManager)
|
||||
)
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
export function vecClose(
|
||||
a: { x: number; y: number; z: number },
|
||||
b: { x: number; y: number; z: number },
|
||||
eps: number
|
||||
): boolean {
|
||||
return (
|
||||
Math.abs(a.x - b.x) < eps &&
|
||||
Math.abs(a.y - b.y) < eps &&
|
||||
Math.abs(a.z - b.z) < eps
|
||||
)
|
||||
}
|
||||
|
||||
export function cameraStatesClose(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
eps: number
|
||||
): boolean {
|
||||
if (
|
||||
a === null ||
|
||||
b === null ||
|
||||
typeof a !== 'object' ||
|
||||
typeof b !== 'object'
|
||||
)
|
||||
return false
|
||||
const ca = a as {
|
||||
position?: { x: number; y: number; z: number }
|
||||
target?: { x: number; y: number; z: number }
|
||||
zoom?: number
|
||||
cameraType?: string
|
||||
}
|
||||
const cb = b as typeof ca
|
||||
if (
|
||||
!ca.position ||
|
||||
!ca.target ||
|
||||
!cb.position ||
|
||||
!cb.target ||
|
||||
ca.cameraType !== cb.cameraType
|
||||
)
|
||||
return false
|
||||
if (Math.abs((ca.zoom ?? 0) - (cb.zoom ?? 0)) > eps) return false
|
||||
return (
|
||||
vecClose(ca.position, cb.position, eps) &&
|
||||
vecClose(ca.target, cb.target, eps)
|
||||
)
|
||||
}
|
||||
|
||||
export class Preview3DPipelineContext {
|
||||
static readonly loadNodeId = '1'
|
||||
static readonly previewNodeId = '2'
|
||||
|
||||
readonly load3d: Load3DHelper
|
||||
readonly preview3d: Load3DHelper
|
||||
|
||||
constructor(readonly comfyPage: ComfyPage) {
|
||||
this.load3d = new Load3DHelper(
|
||||
comfyPage.vueNodes.getNodeLocator(Preview3DPipelineContext.loadNodeId)
|
||||
)
|
||||
this.preview3d = new Load3DHelper(
|
||||
comfyPage.vueNodes.getNodeLocator(Preview3DPipelineContext.previewNodeId)
|
||||
)
|
||||
}
|
||||
|
||||
async getModelFileWidgetValue(nodeId: string): Promise<string> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const w = n?.widgets?.find((x) => x.name === 'model_file')
|
||||
return typeof w?.value === 'string' ? w.value : ''
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
async getLastTimeModelFile(nodeId: string): Promise<string> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const v = n?.properties?.['Last Time Model File']
|
||||
return typeof v === 'string' ? v : ''
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
async getCameraStateFromProperties(nodeId: string): Promise<unknown> {
|
||||
return this.comfyPage.page.evaluate((id) => {
|
||||
const n = window.app!.graph.getNodeById(Number(id))
|
||||
const cfg = n?.properties?.['Camera Config'] as
|
||||
| { state?: unknown }
|
||||
| undefined
|
||||
return cfg?.state ?? null
|
||||
}, nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
export const preview3dPipelineTest = comfyPageFixture.extend<{
|
||||
preview3dPipeline: Preview3DPipelineContext
|
||||
}>({
|
||||
preview3dPipeline: async ({ comfyPage }, use) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
await comfyPage.workflow.loadWorkflow('3d/preview3d_pipeline')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const pipeline = new Preview3DPipelineContext(comfyPage)
|
||||
await use(pipeline)
|
||||
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({})
|
||||
}
|
||||
})
|
||||
209
browser_tests/tests/load3d/preview3dExecution.spec.ts
Normal file
209
browser_tests/tests/load3d/preview3dExecution.spec.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import {
|
||||
cameraStatesClose,
|
||||
preview3dPipelineTest as test,
|
||||
Preview3DPipelineContext,
|
||||
waitForAppBootstrapped
|
||||
} from '@e2e/fixtures/helpers/Preview3DPipelineFixture'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
|
||||
async function seedLoad3dWithCubeObj(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, load3d } = pipeline
|
||||
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
|
||||
await load3d.getUploadButton('upload 3d model').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(assetPath('cube.obj'))
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(Preview3DPipelineContext.loadNodeId),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toContain('cube.obj')
|
||||
|
||||
await load3d.waitForModelLoaded()
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async function setNonDefaultLoad3dCameraState(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, load3d } = pipeline
|
||||
const box = await load3d.canvas.boundingBox()
|
||||
expect(box, 'Load3D canvas bounding box should exist').not.toBeNull()
|
||||
|
||||
const startX = box!.x + box!.width * 0.5
|
||||
const startY = box!.y + box!.height * 0.5
|
||||
await comfyPage.page.mouse.move(startX, startY)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(startX + 80, startY + 20, {
|
||||
steps: 10
|
||||
})
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.loadNodeId
|
||||
),
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.not.toBeNull()
|
||||
}
|
||||
|
||||
async function alignPreview3dWorkflowUiSettings(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
await pipeline.comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await pipeline.comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
}
|
||||
|
||||
test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
test('Preview3D loads model from execution output', async ({
|
||||
preview3dPipeline: pipeline
|
||||
}) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await seedLoad3dWithCubeObj(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 90_000 }
|
||||
)
|
||||
.not.toBe('')
|
||||
|
||||
const modelPath = await pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
await expect(
|
||||
modelPath.length,
|
||||
'Preview3D model path populated'
|
||||
).toBeGreaterThan(4)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getLastTimeModelFile(Preview3DPipelineContext.previewNodeId),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.toBe(modelPath)
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const b = await pipeline.preview3d.canvas.boundingBox()
|
||||
return (b?.width ?? 0) > 0 && (b?.height ?? 0) > 0
|
||||
})
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('Preview3D restores last model and camera after save and full reload', async ({
|
||||
preview3dPipeline: pipeline
|
||||
}) => {
|
||||
test.setTimeout(180_000)
|
||||
|
||||
await seedLoad3dWithCubeObj(pipeline)
|
||||
await setNonDefaultLoad3dCameraState(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 90_000 }
|
||||
)
|
||||
.not.toBe('')
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
const savedPath = await pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.not.toBeNull()
|
||||
|
||||
const savedCamera = await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
|
||||
const workflowName = `p3d-restore-${Date.now().toString(36)}`
|
||||
await pipeline.comfyPage.menu.workflowsTab.open()
|
||||
await pipeline.comfyPage.menu.topbar.saveWorkflow(workflowName)
|
||||
|
||||
await pipeline.comfyPage.page.reload({ waitUntil: 'networkidle' })
|
||||
await waitForAppBootstrapped(pipeline.comfyPage)
|
||||
|
||||
await alignPreview3dWorkflowUiSettings(pipeline)
|
||||
|
||||
const tab = pipeline.comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await tab.getPersistedItem(workflowName).click()
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(15_000)
|
||||
await pipeline.comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.toBe(savedPath)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
pipeline.getLastTimeModelFile(Preview3DPipelineContext.previewNodeId),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.toBe(savedPath)
|
||||
|
||||
await pipeline.preview3d.waitForModelLoaded()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const b = await pipeline.preview3d.canvas.boundingBox()
|
||||
return (b?.width ?? 0) > 0 && (b?.height ?? 0) > 0
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
cameraStatesClose(
|
||||
await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
savedCamera,
|
||||
2e-2
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user