mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-18 11:30:39 +00:00
## Summary
Adds Playwright coverage for `Preview3D execution` and persistence :
real queue execution against a `Load3D → Preview3D` workflow, plus `save
/ full reload / reopen` from the sidebar.
## What these tests do
**Fixture** (every test)
Turns on Vue Nodes, uses the sidebar for workflows, loads a Load3D →
Preview3D workflow, waits for nodes, then clears saved workflows after
the test so runs stay isolated.
**Test 1 — execution updates Preview3D**
Uploads `cube.obj`(the existing test file in the merged version) to
Load3D, runs `Queue Prompt`, then checks that Preview3D’s model_file and
Last Time Model File match and the canvas has non-zero size. No 3D
screenshots (GPU flakiness).
**Test 2 — persistence after reload**
Same upload + queue, then saves the workflow, reloads the page,
re-applies the same UI settings, opens the saved workflow, and checks
the same model path and camera state (with a small numeric tolerance).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Adds new slow, WebGL-dependent E2E tests and fixtures, which can
increase CI runtime and introduce flakiness due to timing/graphics
variability, but does not change production logic.
>
> **Overview**
> Adds a new `Load3D → Preview3D` workflow asset and a dedicated
Playwright fixture (`Preview3DPipelineFixture`) to drive real queue
execution, upload a 3D model, and interact with the 3D canvases (orbit
drags) while asserting `model_file`/`Last Time Model File` and camera
state via node properties.
>
> Introduces camera-state comparison helpers with explicit numeric
tolerances, and adds a new `preview3dExecution.spec.ts` suite that
validates (1) Preview3D updates from execution output and (2) model +
camera persistence across save, full page reload, and reopening the
workflow from the sidebar.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5f54b0f650. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11014-test-add-Preview3D-execution-flow-E2E-tests-33e6d73d3650811fa298c364ae196606)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Terry Jia <terryjia88@gmail.com>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
interface Preview3dCameraStatePayload {
|
|
position: { x: number; y: number; z: number }
|
|
target: { x: number; y: number; z: number }
|
|
zoom?: number
|
|
cameraType?: string
|
|
}
|
|
|
|
type Vec3 = { x: number; y: number; z: number }
|
|
|
|
function isVec3(v: unknown): v is Vec3 {
|
|
if (v === null || typeof v !== 'object') return false
|
|
const r = v as Record<string, unknown>
|
|
return (
|
|
'x' in v &&
|
|
typeof r.x === 'number' &&
|
|
'y' in v &&
|
|
typeof r.y === 'number' &&
|
|
'z' in v &&
|
|
typeof r.z === 'number'
|
|
)
|
|
}
|
|
|
|
function isPreview3dCameraStatePayload(
|
|
v: unknown
|
|
): v is Preview3dCameraStatePayload {
|
|
if (v === null || typeof v !== 'object') return false
|
|
if (!('position' in v) || !('target' in v)) return false
|
|
const r = v as Record<string, unknown>
|
|
return isVec3(r.position) && isVec3(r.target)
|
|
}
|
|
|
|
function vecMaxAbsDelta(a: Vec3, b: Vec3): number {
|
|
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z))
|
|
}
|
|
|
|
function vecWithinEps(a: Vec3, b: Vec3, eps: number): boolean {
|
|
return vecMaxAbsDelta(a, b) <= eps
|
|
}
|
|
|
|
/**
|
|
* 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 (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(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 (
|
|
vecWithinEps(a.position, b.position, PREVIEW3D_CAMERA_AXIS_RESTORE_EPS) &&
|
|
vecWithinEps(a.target, b.target, PREVIEW3D_CAMERA_AXIS_RESTORE_EPS)
|
|
)
|
|
}
|
|
|
|
export function preview3dCameraStatesDiffer(
|
|
a: unknown,
|
|
b: unknown,
|
|
eps: number
|
|
): boolean {
|
|
if (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(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 !(
|
|
vecWithinEps(a.position, b.position, eps) &&
|
|
vecWithinEps(a.target, b.target, eps)
|
|
)
|
|
}
|