mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
test: Preview3D e2e hardening and review follow-ups
- Add ComfyPage.waitForReady; reload uses domcontentloaded then ready wait - NodeReference helpers for widget/properties; object_info skip before workflow load - preview3dRestoreCameraStatesMatch + Vitest; poll messages; drop @node tag - waitForWorkflowIdle optional diagnostic message; vite Vitest include and @e2e alias
This commit is contained in:
@@ -315,13 +315,28 @@ export class ComfyPage {
|
||||
await this.goto()
|
||||
|
||||
await this.page.waitForFunction(() => document.fonts.ready)
|
||||
await this.waitForReady()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the graph view is interactive (extension manager + loading mask).
|
||||
* Use after reload or any navigation that skips {@link setup}.
|
||||
*/
|
||||
async waitForReady(options: { timeout?: number } = {}): Promise<void> {
|
||||
const waitOpts =
|
||||
options.timeout !== undefined ? { timeout: options.timeout } : undefined
|
||||
await this.page.waitForFunction(
|
||||
() =>
|
||||
// window.app => GraphCanvas ready
|
||||
// window.app.extensionManager => GraphView ready
|
||||
window.app && window.app.extensionManager
|
||||
Boolean(window.app?.extensionManager),
|
||||
undefined,
|
||||
waitOpts
|
||||
)
|
||||
await this.page.locator('.p-blockui-mask').waitFor({ state: 'hidden' })
|
||||
await this.page.locator('.p-blockui-mask').waitFor({
|
||||
state: 'hidden',
|
||||
...(options.timeout !== undefined ? { timeout: options.timeout } : {})
|
||||
})
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,31 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
import type { Preview3dE2eCameraState as CameraState } from '@e2e/fixtures/utils/preview3dCameraState'
|
||||
import {
|
||||
isPreview3dE2eCameraState as isCameraState,
|
||||
preview3dCameraStatesDiffer as cameraStatesDiffer
|
||||
} from '@e2e/fixtures/utils/preview3dCameraState'
|
||||
import { Load3DHelper } from '@e2e/tests/load3d/Load3DHelper'
|
||||
|
||||
export async function waitForAppBootstrapped(
|
||||
export type { CameraState }
|
||||
export { isCameraState }
|
||||
|
||||
export async function backendHasPreview3DNodes(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<void> {
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => Boolean(window.app?.extensionManager),
|
||||
undefined,
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', {
|
||||
state: 'hidden',
|
||||
timeout: 30_000
|
||||
): Promise<boolean> {
|
||||
const resp = await comfyPage.request.get(`${comfyPage.url}/object_info`, {
|
||||
failOnStatusCode: false
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
export function vecClose(
|
||||
a: { x: number; y: number; z: number },
|
||||
b: { x: number; y: number; z: number },
|
||||
eps: number
|
||||
): boolean {
|
||||
if (!resp.ok()) return false
|
||||
const data: unknown = await resp.json()
|
||||
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)
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'Load3D' in data &&
|
||||
'Preview3D' in data
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,41 +46,133 @@ export class Preview3DPipelineContext {
|
||||
}
|
||||
|
||||
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)
|
||||
const ref = await this.comfyPage.nodeOps.getNodeRefById(nodeId)
|
||||
const v = await ref.getWidgetValueByName('model_file')
|
||||
return typeof v === 'string' ? v : ''
|
||||
}
|
||||
|
||||
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)
|
||||
const ref = await this.comfyPage.nodeOps.getNodeRefById(nodeId)
|
||||
const v = await ref.getStoredPropertyValue('Last Time Model File')
|
||||
return typeof v === 'string' ? v : ''
|
||||
}
|
||||
|
||||
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)
|
||||
const ref = await this.comfyPage.nodeOps.getNodeRefById(nodeId)
|
||||
const cfg = await ref.getStoredPropertyValue('Camera Config')
|
||||
if (cfg === null || typeof cfg !== 'object') return null
|
||||
const state = Reflect.get(cfg, 'state')
|
||||
return state ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export 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)
|
||||
)
|
||||
.toContain('cube.obj')
|
||||
|
||||
await load3d.waitForModelLoaded()
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
export async function setNonDefaultLoad3dCameraState(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, load3d } = pipeline
|
||||
const initialCamera = await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.loadNodeId
|
||||
)
|
||||
const box = await load3d.canvas.boundingBox()
|
||||
expect(box, 'Load3D canvas bounding box should exist').not.toBeNull()
|
||||
|
||||
const w = box!.width
|
||||
const h = box!.height
|
||||
await load3d.canvas.dragTo(load3d.canvas, {
|
||||
sourcePosition: { x: w / 2, y: h / 2 },
|
||||
targetPosition: { x: w / 2 + 80, y: h / 2 + 20 },
|
||||
steps: 10,
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const current = await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.loadNodeId
|
||||
)
|
||||
if (current === null) return false
|
||||
if (initialCamera === null) return true
|
||||
return cameraStatesDiffer(current, initialCamera, 1e-4)
|
||||
},
|
||||
{
|
||||
timeout: 10_000,
|
||||
message:
|
||||
'Load3D camera state should change after orbit drag (see cameraStatesDiffer)'
|
||||
}
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
export async function nudgePreview3dCameraIntoProperties(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, preview3d } = pipeline
|
||||
const box = await preview3d.canvas.boundingBox()
|
||||
expect(box, 'Preview3D canvas bounding box should exist').not.toBeNull()
|
||||
|
||||
const w = box!.width
|
||||
const h = box!.height
|
||||
await preview3d.canvas.dragTo(preview3d.canvas, {
|
||||
sourcePosition: { x: w / 2, y: h / 2 },
|
||||
targetPosition: { x: w / 2 - 60, y: h / 2 + 20 },
|
||||
steps: 10,
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
export 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'
|
||||
)
|
||||
}
|
||||
|
||||
export const preview3dPipelineTest = comfyPageFixture.extend<{
|
||||
preview3dPipeline: Preview3DPipelineContext
|
||||
}>({
|
||||
preview3dPipeline: async ({ comfyPage }, use) => {
|
||||
preview3dPipeline: async ({ comfyPage }, use, testInfo) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
|
||||
const hasPreview3dNodes = await backendHasPreview3DNodes(comfyPage)
|
||||
if (!hasPreview3dNodes) {
|
||||
testInfo.skip(
|
||||
true,
|
||||
'Requires ComfyUI backend with Load3D and Preview3D nodes (object_info)'
|
||||
)
|
||||
await use(new Preview3DPipelineContext(comfyPage))
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({})
|
||||
return
|
||||
}
|
||||
|
||||
await comfyPage.workflow.loadWorkflow('3d/preview3d_pipeline')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
|
||||
@@ -165,14 +165,27 @@ export class WorkflowHelper {
|
||||
})
|
||||
}
|
||||
|
||||
async waitForWorkflowIdle(timeout = 5000): Promise<void> {
|
||||
await this.comfyPage.page.waitForFunction(
|
||||
() =>
|
||||
!(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
|
||||
?.isBusy,
|
||||
undefined,
|
||||
{ timeout }
|
||||
)
|
||||
async waitForWorkflowIdle(
|
||||
timeout = 5000,
|
||||
options?: { message?: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.comfyPage.page.waitForFunction(
|
||||
() =>
|
||||
!(window.app?.extensionManager as WorkspaceStore | undefined)
|
||||
?.workflow?.isBusy,
|
||||
undefined,
|
||||
{ timeout }
|
||||
)
|
||||
} catch (err) {
|
||||
if (options?.message) {
|
||||
throw new Error(
|
||||
`${options.message} (timed out after ${timeout}ms waiting for workflow idle)`,
|
||||
{ cause: err }
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getExportedWorkflow(options: { api: true }): Promise<ComfyApiWorkflow>
|
||||
|
||||
@@ -345,6 +345,32 @@ export class NodeReference {
|
||||
[this.id, prop] as const
|
||||
)
|
||||
}
|
||||
|
||||
async getWidgetValueByName(name: string): Promise<unknown> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, name]) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const widget = node.widgets?.find((w) => w.name === name)
|
||||
return widget?.value
|
||||
},
|
||||
[this.id, name] as const
|
||||
)
|
||||
}
|
||||
|
||||
async getStoredPropertyValue(key: string): Promise<unknown> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, key]) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const bag = node.properties
|
||||
if (bag === null || typeof bag !== 'object') return undefined
|
||||
return Reflect.get(bag, key)
|
||||
},
|
||||
[this.id, key] as const
|
||||
)
|
||||
}
|
||||
|
||||
async getOutput(index: number) {
|
||||
return new NodeSlotReference('output', index, this)
|
||||
}
|
||||
|
||||
67
browser_tests/fixtures/utils/preview3dCameraState.test.ts
Normal file
67
browser_tests/fixtures/utils/preview3dCameraState.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
PREVIEW3D_CAMERA_AXIS_RESTORE_EPS,
|
||||
preview3dCameraStatesDiffer,
|
||||
preview3dRestoreCameraStatesMatch
|
||||
} from '@e2e/fixtures/utils/preview3dCameraState'
|
||||
|
||||
const base = {
|
||||
cameraType: 'perspective',
|
||||
position: { x: 1, y: 2, z: 3 },
|
||||
target: { x: 0, y: 0, z: 0 },
|
||||
zoom: 1
|
||||
}
|
||||
|
||||
describe('preview3dRestoreCameraStatesMatch', () => {
|
||||
it('returns true for identical states', () => {
|
||||
expect(preview3dRestoreCameraStatesMatch(base, { ...base })).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for invalid payloads', () => {
|
||||
expect(preview3dRestoreCameraStatesMatch(null, base)).toBe(false)
|
||||
expect(preview3dRestoreCameraStatesMatch(base, { position: 'nope' })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false when cameraType differs', () => {
|
||||
expect(
|
||||
preview3dRestoreCameraStatesMatch(base, { ...base, cameraType: 'ortho' })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts axis drift within PREVIEW3D_CAMERA_AXIS_RESTORE_EPS', () => {
|
||||
const drifted = {
|
||||
...base,
|
||||
position: {
|
||||
x: base.position.x + PREVIEW3D_CAMERA_AXIS_RESTORE_EPS * 0.9,
|
||||
y: base.position.y,
|
||||
z: base.position.z
|
||||
}
|
||||
}
|
||||
expect(preview3dRestoreCameraStatesMatch(base, drifted)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects axis drift beyond PREVIEW3D_CAMERA_AXIS_RESTORE_EPS', () => {
|
||||
const drifted = {
|
||||
...base,
|
||||
position: {
|
||||
x: base.position.x + PREVIEW3D_CAMERA_AXIS_RESTORE_EPS * 1.1,
|
||||
y: base.position.y,
|
||||
z: base.position.z
|
||||
}
|
||||
}
|
||||
expect(preview3dRestoreCameraStatesMatch(base, drifted)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('preview3dCameraStatesDiffer', () => {
|
||||
it('treats missing typed state as different', () => {
|
||||
expect(preview3dCameraStatesDiffer(null, base, 1e-4)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when states are equal within eps', () => {
|
||||
expect(preview3dCameraStatesDiffer(base, { ...base }, 1e-4)).toBe(false)
|
||||
})
|
||||
})
|
||||
91
browser_tests/fixtures/utils/preview3dCameraState.ts
Normal file
91
browser_tests/fixtures/utils/preview3dCameraState.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export interface Preview3dE2eCameraState {
|
||||
position: { x: number; y: number; z: number }
|
||||
target: { x: number; y: number; z: number }
|
||||
zoom?: number
|
||||
cameraType?: string
|
||||
}
|
||||
|
||||
function isVec3(v: unknown): v is { x: number; y: number; z: number } {
|
||||
if (v === null || typeof v !== 'object') return false
|
||||
const x = Reflect.get(v, 'x')
|
||||
const y = Reflect.get(v, 'y')
|
||||
const z = Reflect.get(v, 'z')
|
||||
return typeof x === 'number' && typeof y === 'number' && typeof z === 'number'
|
||||
}
|
||||
|
||||
export function isPreview3dE2eCameraState(
|
||||
v: unknown
|
||||
): v is Preview3dE2eCameraState {
|
||||
if (v === null || typeof v !== 'object') return false
|
||||
const position = Reflect.get(v, 'position')
|
||||
const target = Reflect.get(v, 'target')
|
||||
return isVec3(position) && isVec3(target)
|
||||
}
|
||||
|
||||
function vecMaxAbsDelta(
|
||||
a: { x: number; y: number; z: number },
|
||||
b: { x: number; y: number; z: number }
|
||||
): number {
|
||||
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z))
|
||||
}
|
||||
|
||||
/**
|
||||
* Max abs error per position/target axis when comparing restored Preview3D
|
||||
* camera state (same order of magnitude as the former 2e-2 poll tolerance).
|
||||
*/
|
||||
export const PREVIEW3D_CAMERA_AXIS_RESTORE_EPS = 0.02
|
||||
|
||||
/**
|
||||
* Max abs zoom error when comparing restored Preview3D state (aligned with
|
||||
* Playwright `toBeCloseTo(..., 5)`-style checks on typical zoom magnitudes).
|
||||
*/
|
||||
export const PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS = 1e-4
|
||||
|
||||
export function preview3dRestoreCameraStatesMatch(
|
||||
a: unknown,
|
||||
b: unknown
|
||||
): boolean {
|
||||
if (!isPreview3dE2eCameraState(a) || !isPreview3dE2eCameraState(b)) {
|
||||
return false
|
||||
}
|
||||
if (a.cameraType !== b.cameraType) return false
|
||||
const zoomA = typeof a.zoom === 'number' ? a.zoom : 0
|
||||
const zoomB = typeof b.zoom === 'number' ? b.zoom : 0
|
||||
if (Math.abs(zoomA - zoomB) > PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
vecMaxAbsDelta(a.position, b.position) <=
|
||||
PREVIEW3D_CAMERA_AXIS_RESTORE_EPS &&
|
||||
vecMaxAbsDelta(a.target, b.target) <= PREVIEW3D_CAMERA_AXIS_RESTORE_EPS
|
||||
)
|
||||
}
|
||||
|
||||
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 preview3dCameraStatesDiffer(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
eps: number
|
||||
): boolean {
|
||||
if (!isPreview3dE2eCameraState(a) || !isPreview3dE2eCameraState(b)) {
|
||||
return true
|
||||
}
|
||||
if (a.cameraType !== b.cameraType) return true
|
||||
const zoomA = typeof a.zoom === 'number' ? a.zoom : 0
|
||||
const zoomB = typeof b.zoom === 'number' ? b.zoom : 0
|
||||
if (Math.abs(zoomA - zoomB) > eps) return true
|
||||
return !(
|
||||
vecClose(a.position, b.position, eps) && vecClose(a.target, b.target, eps)
|
||||
)
|
||||
}
|
||||
@@ -1,85 +1,20 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import {
|
||||
cameraStatesClose,
|
||||
alignPreview3dWorkflowUiSettings,
|
||||
nudgePreview3dCameraIntoProperties,
|
||||
preview3dPipelineTest as test,
|
||||
Preview3DPipelineContext,
|
||||
waitForAppBootstrapped
|
||||
seedLoad3dWithCubeObj,
|
||||
setNonDefaultLoad3dCameraState
|
||||
} from '@e2e/fixtures/helpers/Preview3DPipelineFixture'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
import {
|
||||
PREVIEW3D_CAMERA_AXIS_RESTORE_EPS,
|
||||
PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS,
|
||||
preview3dRestoreCameraStatesMatch
|
||||
} from '@e2e/fixtures/utils/preview3dCameraState'
|
||||
|
||||
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)
|
||||
)
|
||||
.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 w = box!.width
|
||||
const h = box!.height
|
||||
await load3d.canvas.dragTo(load3d.canvas, {
|
||||
sourcePosition: { x: w / 2, y: h / 2 },
|
||||
targetPosition: { x: w / 2 + 80, y: h / 2 + 20 },
|
||||
steps: 10,
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
pipeline.getCameraStateFromProperties(Preview3DPipelineContext.loadNodeId)
|
||||
)
|
||||
.not.toBeNull()
|
||||
}
|
||||
|
||||
async function nudgePreview3dCameraIntoProperties(
|
||||
pipeline: Preview3DPipelineContext
|
||||
): Promise<void> {
|
||||
const { comfyPage, preview3d } = pipeline
|
||||
const box = await preview3d.canvas.boundingBox()
|
||||
expect(box, 'Preview3D canvas bounding box should exist').not.toBeNull()
|
||||
|
||||
const w = box!.width
|
||||
const h = box!.height
|
||||
await preview3d.canvas.dragTo(preview3d.canvas, {
|
||||
sourcePosition: { x: w / 2, y: h / 2 },
|
||||
targetPosition: { x: w / 2 - 60, y: h / 2 + 20 },
|
||||
steps: 10,
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
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.describe('Preview3D execution flow', { tag: ['@slow'] }, () => {
|
||||
test('Preview3D loads model from execution output', async ({
|
||||
preview3dPipeline: pipeline
|
||||
}) => {
|
||||
@@ -88,7 +23,10 @@ test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
await seedLoad3dWithCubeObj(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(90_000)
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(90_000, {
|
||||
message:
|
||||
'QueuePrompt did not finish: backend needs Load3D and Preview3D nodes and a working execution environment'
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -99,7 +37,7 @@ test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
const modelPath = await pipeline.getModelFileWidgetValue(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
)
|
||||
await expect(
|
||||
expect(
|
||||
modelPath.length,
|
||||
'Preview3D model path populated'
|
||||
).toBeGreaterThan(4)
|
||||
@@ -129,7 +67,10 @@ test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
await setNonDefaultLoad3dCameraState(pipeline)
|
||||
|
||||
await pipeline.comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(90_000)
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(90_000, {
|
||||
message:
|
||||
'QueuePrompt did not finish: backend needs Load3D and Preview3D nodes and a working execution environment'
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -164,15 +105,18 @@ test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
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 pipeline.comfyPage.page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await pipeline.comfyPage.waitForReady({ timeout: 30_000 })
|
||||
|
||||
await alignPreview3dWorkflowUiSettings(pipeline)
|
||||
|
||||
const tab = pipeline.comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await tab.getPersistedItem(workflowName).click()
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(30_000)
|
||||
await pipeline.comfyPage.workflow.waitForWorkflowIdle(30_000, {
|
||||
message:
|
||||
'Workflow did not settle after opening saved workflow from sidebar (Preview3D restore test)'
|
||||
})
|
||||
await pipeline.comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await expect
|
||||
@@ -197,14 +141,18 @@ test.describe('Preview3D execution flow', { tag: ['@node', '@slow'] }, () => {
|
||||
.toBe(true)
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
cameraStatesClose(
|
||||
await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
.poll(
|
||||
async () =>
|
||||
preview3dRestoreCameraStatesMatch(
|
||||
await pipeline.getCameraStateFromProperties(
|
||||
Preview3DPipelineContext.previewNodeId
|
||||
),
|
||||
savedCamera
|
||||
),
|
||||
savedCamera,
|
||||
2e-2
|
||||
)
|
||||
{
|
||||
timeout: 15_000,
|
||||
message: `Preview3D camera after reload should match saved state (axis max delta ≤ ${PREVIEW3D_CAMERA_AXIS_RESTORE_EPS}, zoom delta ≤ ${PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS}; see preview3dCameraState.test.ts)`
|
||||
}
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
@@ -2,6 +2,8 @@ import { sentryVitePlugin } from '@sentry/vite-plugin'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { execSync } from 'child_process'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { config as dotenvConfig } from 'dotenv'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import { Readable } from 'stream'
|
||||
@@ -19,6 +21,8 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
import { comfyAPIPlugin } from './build/plugins'
|
||||
|
||||
const projectRoot = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
dotenvConfig()
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV === 'development'
|
||||
@@ -634,7 +638,8 @@ export default defineConfig({
|
||||
'@/utils/formatUtil': '/packages/shared-frontend-utils/src/formatUtil.ts',
|
||||
'@/utils/networkUtil':
|
||||
'/packages/shared-frontend-utils/src/networkUtil.ts',
|
||||
'@': '/src'
|
||||
'@': '/src',
|
||||
'@e2e': path.join(projectRoot, 'browser_tests')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -652,7 +657,8 @@ export default defineConfig({
|
||||
include: [
|
||||
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
|
||||
'scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'browser_tests/fixtures/utils/**/*.test.ts'
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
|
||||
Reference in New Issue
Block a user