mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 05:19:03 +00:00
Compare commits
3 Commits
glary/raf-
...
test/load3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23cee5434b | ||
|
|
d7b360c9fe | ||
|
|
d07599024a |
10
browser_tests/assets/3d/test_model.ply
Normal file
10
browser_tests/assets/3d/test_model.ply
Normal file
@@ -0,0 +1,10 @@
|
||||
ply
|
||||
format ascii 1.0
|
||||
element vertex 3
|
||||
property float x
|
||||
property float y
|
||||
property float z
|
||||
end_header
|
||||
0.0 0.0 0.0
|
||||
1.0 0.0 0.0
|
||||
0.0 1.0 0.0
|
||||
BIN
browser_tests/assets/3d/test_model.splat
Normal file
BIN
browser_tests/assets/3d/test_model.splat
Normal file
Binary file not shown.
@@ -23,3 +23,15 @@ export const load3dViewerTest = load3dTest.extend<{
|
||||
await use(new Load3DViewerHelper(comfyPage.page))
|
||||
}
|
||||
})
|
||||
|
||||
export const load3dVueEnabledTest = comfyPageFixture.extend<{
|
||||
enableVueNodes: void
|
||||
}>({
|
||||
enableVueNodes: [
|
||||
async ({ comfyPage }, use) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await use()
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
@@ -38,6 +38,23 @@ export class Load3DHelper {
|
||||
await this.menuButton.click()
|
||||
}
|
||||
|
||||
async selectMenuCategory(name: string): Promise<void> {
|
||||
await this.getMenuCategory(name).click()
|
||||
}
|
||||
|
||||
async clickGridToggle(): Promise<void> {
|
||||
await this.node.getByRole('button', { name: /show grid/i }).click()
|
||||
}
|
||||
|
||||
async switchCameraType(): Promise<void> {
|
||||
await this.node.getByRole('button', { name: /switch camera/i }).click()
|
||||
}
|
||||
|
||||
async selectMaterialMode(mode: string): Promise<void> {
|
||||
await this.node.getByRole('button', { name: /material mode/i }).click()
|
||||
await this.node.getByRole('button', { name: mode, exact: true }).click()
|
||||
}
|
||||
|
||||
async setBackgroundColor(hex: string): Promise<void> {
|
||||
await this.colorInput.evaluate((el, value) => {
|
||||
;(el as HTMLInputElement).value = value
|
||||
@@ -51,3 +68,20 @@ export class Load3DHelper {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNodeConfig<T>(
|
||||
page: Page,
|
||||
nodeId: string,
|
||||
configKey: string
|
||||
): Promise<T | null> {
|
||||
return page.evaluate(
|
||||
({ id, key }) => {
|
||||
const node = window.app!.graph.getNodeById(Number(id))
|
||||
if (!node?.properties) return null
|
||||
return (
|
||||
((node.properties as Record<string, unknown>)[key] as T | null) ?? null
|
||||
)
|
||||
},
|
||||
{ id: nodeId, key: configKey }
|
||||
)
|
||||
}
|
||||
|
||||
149
browser_tests/tests/load3d/load3dConfiguration.spec.ts
Normal file
149
browser_tests/tests/load3d/load3dConfiguration.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { load3dTest as test } from '@e2e/fixtures/helpers/Load3DFixtures'
|
||||
import type { Load3DHelper } from '@e2e/tests/load3d/Load3DHelper'
|
||||
import { getNodeConfig } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
const nodeId = '1'
|
||||
|
||||
async function saveAndReload(comfyPage: ComfyPage): Promise<void> {
|
||||
const name = `load3d-config-${Date.now().toString(36)}`
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.topbar.saveWorkflow(name)
|
||||
await comfyPage.page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window.app && window.app.extensionManager,
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
await comfyPage.page.locator('.p-blockui-mask').waitFor({
|
||||
state: 'hidden',
|
||||
timeout: 30_000
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await tab.getPersistedItem(name).click()
|
||||
await comfyPage.workflow.waitForWorkflowIdle(30_000)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
|
||||
async function openMenuAtCategory(
|
||||
load3d: Load3DHelper,
|
||||
category: string
|
||||
): Promise<void> {
|
||||
await load3d.openMenu()
|
||||
await load3d.selectMenuCategory(category)
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Load3D configuration persistence',
|
||||
{ tag: ['@workflow', '@slow'] },
|
||||
() => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('scene config (grid + background color) persists across save and reload', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await openMenuAtCategory(load3d, 'Scene')
|
||||
await load3d.clickGridToggle()
|
||||
await load3d.setBackgroundColor('#ff4400')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Scene Config'
|
||||
).then((c) => c?.showGrid)
|
||||
)
|
||||
.toBe(false)
|
||||
|
||||
await saveAndReload(comfyPage)
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Scene Config'
|
||||
)
|
||||
)
|
||||
.toMatchObject({ showGrid: false, backgroundColor: '#ff4400' })
|
||||
})
|
||||
|
||||
test('camera config (orthographic) persists across save and reload', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await openMenuAtCategory(load3d, 'Camera')
|
||||
await load3d.switchCameraType()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Camera Config'
|
||||
).then((c) => c?.cameraType)
|
||||
)
|
||||
.toBe('orthographic')
|
||||
|
||||
await saveAndReload(comfyPage)
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Camera Config'
|
||||
).then((c) => c?.cameraType)
|
||||
)
|
||||
.toBe('orthographic')
|
||||
})
|
||||
|
||||
test('model config (wireframe material) persists across save and reload', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
await openMenuAtCategory(load3d, 'Model')
|
||||
await load3d.selectMaterialMode('Wireframe')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Model Config'
|
||||
).then((c) => c?.materialMode)
|
||||
)
|
||||
.toBe('wireframe')
|
||||
|
||||
await saveAndReload(comfyPage)
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Model Config'
|
||||
).then((c) => c?.materialMode)
|
||||
)
|
||||
.toBe('wireframe')
|
||||
})
|
||||
}
|
||||
)
|
||||
116
browser_tests/tests/load3d/load3dSettings.spec.ts
Normal file
116
browser_tests/tests/load3d/load3dSettings.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import {
|
||||
load3dTest,
|
||||
load3dVueEnabledTest
|
||||
} from '@e2e/fixtures/helpers/Load3DFixtures'
|
||||
import { getNodeConfig } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
async function addLoad3dNode(page: Page): Promise<string> {
|
||||
const nodeId = await page.evaluate(() => {
|
||||
const node = window.app!.graph.add(
|
||||
window.LiteGraph!.createNode('Load3D', undefined, {})
|
||||
)
|
||||
return String(node!.id)
|
||||
})
|
||||
await page.waitForFunction(
|
||||
(id) => document.querySelector(`[data-node-id="${id}"]`) !== null,
|
||||
nodeId
|
||||
)
|
||||
return nodeId
|
||||
}
|
||||
|
||||
load3dVueEnabledTest.describe(
|
||||
'Load3D settings integration',
|
||||
{ tag: '@ui' },
|
||||
() => {
|
||||
load3dVueEnabledTest(
|
||||
'default grid visibility is applied to new nodes from setting',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Load3D.ShowGrid', false)
|
||||
const nodeId = await addLoad3dNode(comfyPage.page)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Scene Config'
|
||||
).then((c) => c?.showGrid)
|
||||
)
|
||||
.toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
load3dVueEnabledTest(
|
||||
'default background color is applied to new nodes from setting',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Load3D.BackgroundColor',
|
||||
'ff4400'
|
||||
)
|
||||
const nodeId = await addLoad3dNode(comfyPage.page)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Scene Config'
|
||||
).then((c) => c?.backgroundColor)
|
||||
)
|
||||
.toBe('#ff4400')
|
||||
}
|
||||
)
|
||||
|
||||
load3dVueEnabledTest(
|
||||
'default camera type is applied to new nodes from setting',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Load3D.CameraType',
|
||||
'orthographic'
|
||||
)
|
||||
const nodeId = await addLoad3dNode(comfyPage.page)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
getNodeConfig<Record<string, unknown>>(
|
||||
comfyPage.page,
|
||||
nodeId,
|
||||
'Camera Config'
|
||||
).then((c) => c?.cameraType)
|
||||
)
|
||||
.toBe('orthographic')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
load3dTest.describe(
|
||||
'3D Viewer button visibility controlled by setting',
|
||||
{ tag: '@ui' },
|
||||
() => {
|
||||
load3dTest(
|
||||
'open viewer button visible when 3DViewerEnable is true',
|
||||
async ({ comfyPage, load3d }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Load3D.3DViewerEnable', true)
|
||||
await expect(load3d.openViewerButton).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
load3dTest(
|
||||
'open viewer button hidden when 3DViewerEnable is false',
|
||||
async ({ comfyPage, load3d }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Load3D.3DViewerEnable',
|
||||
false
|
||||
)
|
||||
await expect(load3d.openViewerButton).toBeHidden()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
79
browser_tests/tests/load3d/load3dSplatPLY.spec.ts
Normal file
79
browser_tests/tests/load3d/load3dSplatPLY.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
import { load3dTest as test } from '@e2e/fixtures/helpers/Load3DFixtures'
|
||||
import type { Load3DHelper } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
async function uploadModel(
|
||||
comfyPage: ComfyPage,
|
||||
load3d: Load3DHelper,
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
const uploadDone = comfyPage.page.waitForResponse(
|
||||
(r) => r.url().includes('/upload/') && r.status() === 200,
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
const chooser = comfyPage.page.waitForEvent('filechooser')
|
||||
await load3d.getUploadButton('upload 3d model').click()
|
||||
const fc = await chooser
|
||||
await fc.setFiles(filePath)
|
||||
await uploadDone
|
||||
}
|
||||
|
||||
test.describe('Load3D PLY model loading', { tag: ['@node'] }, () => {
|
||||
test('loads a PLY model with the threejs engine', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Load3D.PLYEngine', 'threejs')
|
||||
|
||||
await uploadModel(comfyPage, load3d, assetPath('3d/test_model.ply'))
|
||||
|
||||
const nodeRef = await comfyPage.nodeOps.getNodeRefById(1)
|
||||
const modelFileWidget = await nodeRef.getWidget(0)
|
||||
await expect
|
||||
.poll(() => modelFileWidget.getValue())
|
||||
.toContain('test_model.ply')
|
||||
|
||||
await load3d.waitForModelLoaded()
|
||||
|
||||
const canvasBox = await load3d.canvas.boundingBox()
|
||||
expect(
|
||||
canvasBox,
|
||||
'canvas bounding box must exist after model load'
|
||||
).not.toBeNull()
|
||||
expect(canvasBox!.width).toBeGreaterThan(0)
|
||||
expect(canvasBox!.height).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe(
|
||||
'Load3D splat model — controls menu',
|
||||
{ tag: ['@node', '@slow'] },
|
||||
() => {
|
||||
test('Light and Export categories are hidden after loading a splat model', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
test.setTimeout(90_000)
|
||||
|
||||
await uploadModel(comfyPage, load3d, assetPath('3d/test_model.splat'))
|
||||
|
||||
const nodeRef = await comfyPage.nodeOps.getNodeRefById(1)
|
||||
const modelFileWidget = await nodeRef.getWidget(0)
|
||||
await expect
|
||||
.poll(() => modelFileWidget.getValue())
|
||||
.toContain('test_model.splat')
|
||||
|
||||
await load3d.waitForModelLoaded()
|
||||
await load3d.openMenu()
|
||||
|
||||
await expect(load3d.getMenuCategory('Scene')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Model')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Camera')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Light')).toBeHidden()
|
||||
await expect(load3d.getMenuCategory('Export')).toBeHidden()
|
||||
})
|
||||
}
|
||||
)
|
||||
119
src/extensions/core/load3d/Load3DConfiguration.test.ts
Normal file
119
src/extensions/core/load3d/Load3DConfiguration.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import type {
|
||||
CameraConfig,
|
||||
SceneConfig
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { Dictionary } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
const mockGet = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({ get: mockGet })
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: { apiURL: vi.fn((p: string) => p) }
|
||||
}))
|
||||
|
||||
vi.mock('@/extensions/core/load3d/Load3dUtils', () => ({
|
||||
default: {
|
||||
getResourceURL: vi.fn(() => '/test'),
|
||||
splitFilePath: vi.fn(() => ['', 'test.glb']),
|
||||
getFilenameExtension: vi.fn(() => 'glb')
|
||||
}
|
||||
}))
|
||||
|
||||
function makeLoad3dMock() {
|
||||
return {
|
||||
toggleGrid: vi.fn(),
|
||||
setBackgroundColor: vi.fn(),
|
||||
toggleCamera: vi.fn(),
|
||||
setFOV: vi.fn(),
|
||||
setLightIntensity: vi.fn()
|
||||
} as unknown as InstanceType<
|
||||
typeof import('@/extensions/core/load3d/Load3d').default
|
||||
>
|
||||
}
|
||||
|
||||
function defaultSettings(overrides: Record<string, unknown> = {}) {
|
||||
const base: Record<string, unknown> = {
|
||||
'Comfy.Load3D.ShowGrid': true,
|
||||
'Comfy.Load3D.BackgroundColor': '000000',
|
||||
'Comfy.Load3D.CameraType': 'perspective',
|
||||
'Comfy.Load3D.LightIntensity': 1
|
||||
}
|
||||
return { ...base, ...overrides }
|
||||
}
|
||||
|
||||
describe('Load3DConfiguration — setupDefaultProperties persistence', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
function configure(
|
||||
properties: Dictionary<NodeProperty | undefined>,
|
||||
settingOverrides: Record<string, unknown> = {}
|
||||
) {
|
||||
const settings = defaultSettings(settingOverrides)
|
||||
mockGet.mockImplementation((key: string) => settings[key])
|
||||
const config = new Load3DConfiguration(makeLoad3dMock(), properties)
|
||||
config.configureForSaveMesh('input', '')
|
||||
}
|
||||
|
||||
describe('Scene Config', () => {
|
||||
it('writes Scene Config from settings when property is absent', () => {
|
||||
const properties: Dictionary<NodeProperty | undefined> = {}
|
||||
configure(properties, {
|
||||
'Comfy.Load3D.ShowGrid': false,
|
||||
'Comfy.Load3D.BackgroundColor': 'ff4400'
|
||||
})
|
||||
|
||||
const scene = properties['Scene Config'] as SceneConfig
|
||||
expect(scene).toBeDefined()
|
||||
expect(scene.showGrid).toBe(false)
|
||||
expect(scene.backgroundColor).toBe('#ff4400')
|
||||
})
|
||||
|
||||
it('does not overwrite existing Scene Config', () => {
|
||||
const existing: SceneConfig = {
|
||||
showGrid: true,
|
||||
backgroundColor: '#aabbcc',
|
||||
backgroundImage: ''
|
||||
}
|
||||
const properties: Dictionary<NodeProperty | undefined> = {
|
||||
'Scene Config': existing
|
||||
}
|
||||
configure(properties, { 'Comfy.Load3D.ShowGrid': false })
|
||||
|
||||
expect(properties['Scene Config']).toBe(existing)
|
||||
expect((properties['Scene Config'] as SceneConfig).showGrid).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Camera Config', () => {
|
||||
it('writes Camera Config from settings when property is absent', () => {
|
||||
const properties: Dictionary<NodeProperty | undefined> = {}
|
||||
configure(properties, { 'Comfy.Load3D.CameraType': 'orthographic' })
|
||||
|
||||
const camera = properties['Camera Config'] as CameraConfig
|
||||
expect(camera).toBeDefined()
|
||||
expect(camera.cameraType).toBe('orthographic')
|
||||
})
|
||||
|
||||
it('does not overwrite existing Camera Config', () => {
|
||||
const existing: CameraConfig = { cameraType: 'perspective', fov: 60 }
|
||||
const properties: Dictionary<NodeProperty | undefined> = {
|
||||
'Camera Config': existing
|
||||
}
|
||||
configure(properties, { 'Comfy.Load3D.CameraType': 'orthographic' })
|
||||
|
||||
expect(properties['Camera Config']).toBe(existing)
|
||||
expect((properties['Camera Config'] as CameraConfig).cameraType).toBe(
|
||||
'perspective'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -108,9 +108,15 @@ class Load3DConfiguration {
|
||||
private setupDefaultProperties(bgImagePath?: string) {
|
||||
const sceneConfig = this.loadSceneConfig()
|
||||
this.applySceneConfig(sceneConfig, bgImagePath)
|
||||
if (this.properties && !('Scene Config' in this.properties)) {
|
||||
this.properties['Scene Config'] = sceneConfig
|
||||
}
|
||||
|
||||
const cameraConfig = this.loadCameraConfig()
|
||||
this.applyCameraConfig(cameraConfig)
|
||||
if (this.properties && !('Camera Config' in this.properties)) {
|
||||
this.properties['Camera Config'] = cameraConfig
|
||||
}
|
||||
|
||||
const lightConfig = this.loadLightConfig()
|
||||
this.applyLightConfig(lightConfig)
|
||||
|
||||
Reference in New Issue
Block a user