mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-10 17:17:55 +00:00
## Summary Collapse the OSS vs Cloud branching in the mask editor and painter uploads so both runtimes use the same contract — POST to `/upload/image` with `type: input` and no `subfolder`, then reference the result by filename only. ## Related - Companion spec change (Comfy-Org/ComfyUI): https://github.com/Comfy-Org/ComfyUI/pull/13968 deprecates `/api/upload/mask` and documents `/api/upload/image` as the unified upload contract. No runtime behavior changes on the server. ## Changes - **What**: - `useMaskEditorSaver.ts`: replace the separate `uploadMask` + `uploadImage` helpers with a single `uploadLayer`. All four layers (masked, paint, painted, paintedMasked) now go through `/upload/image`. Dropped the `original_ref` form field and the `clipspace` subfolder. `uploadLayer` throws on a non-ok status, on a non-JSON response body, and on a 200 response missing `data.name` (no more silent fallback to the pre-upload ref). - `usePainter.ts`: removed the runtime branch on upload type/subfolder and on the returned widget value. Always uploads as `type: input` with no subfolder; widget value is `${filename} [input]`. Added a `data.name` guard alongside the existing non-ok and JSON-parse guards. - `usePainter.test.ts`: assert the upload FormData carries `type=input` with no `subfolder`, plus new coverage for the missing-name and JSON-parse-failure error branches. - `browser_tests/tests/maskEditor.spec.ts`: drop the now-dead `**/upload/mask` Playwright route interceptors and tighten the save-success assertion to confirm exactly four `/upload/image` calls (one per layer). - **Breaking**: yes for downstream consumers — the mask editor no longer calls `/upload/mask`, and saved widget values for both editors no longer contain a subfolder prefix. Existing nodes will continue to load their referenced inputs because the filename is preserved; new saves emit the unified shape. ## Review Focus - Confirm the mask editor's four-layer upload (masked, paint, painted, paintedMasked) is still correct without `original_ref` — the layers are now independent uploads rather than alpha-composited server-side. The four blobs are composited client-side in `prepareOutputData` before upload, so the previous `original_ref` chain was vestigial — but worth a second look. - OSS-side painter widget back-compat: previously stored as `painter/foo.png [temp]`, new saves emit `foo.png [input]`. The old format remains valid in saved workflows; only the *new save* shape changes. Loading a pre-existing OSS workflow with a `painter/foo.png [temp]` widget value still resolves via the existing parser path. ## Test plan Unit: - [x] `usePainter.test.ts` — 30 tests pass, including the three new ones covering FormData payload shape, missing `data.name`, and bad JSON. E2E: - [x] `browser_tests/tests/maskEditor.spec.ts` — `save uploads all layers and closes dialog` and `save failure keeps dialog open` updated to the unified contract. Smoke-tested manually against a Cloud staging instance: - [x] **Painter node**: paint strokes → save → reload workflow → widget value resolves and the canvas re-renders cleanly. Upload responds with `subfolder: ""`, `type: "input"`. - [x] **Mask editor**: open editor with a base image, paint + mask, save. All four layers (`clipspace-mask-*`, `clipspace-paint-*`, `clipspace-painted-*`, `clipspace-painted-masked-*`) POST successfully to `/upload/image` and return 200 with the asset-aware response shape (`{ name, subfolder: "", type: "input", asset: { id, hash, tags } }`). The resulting `LoadImage` node references the painted-masked filename by basename only and re-renders. Not validated here: OSS ComfyUI core-side smoke. The contract is symmetric (the OSS `/upload/image` endpoint accepts the same fields the FE now sends), but a smoke against OSS HEAD is recommended before merge. --------- Co-authored-by: GitHub Action <action@github.com>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import { maskEditorTest as test } from '@e2e/fixtures/helpers/MaskEditorHelper'
|
|
|
|
interface UploadResponse {
|
|
name: string
|
|
subfolder: string
|
|
type: 'input' | 'output' | 'temp'
|
|
}
|
|
|
|
const IMAGE_CANVAS_INDEX = 0
|
|
const MASK_CANVAS_INDEX = 2
|
|
|
|
const successResponse = (name: string): UploadResponse => ({
|
|
name,
|
|
subfolder: 'clipspace',
|
|
type: 'input'
|
|
})
|
|
|
|
const fulfillJson = (body: UploadResponse) => ({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(body)
|
|
})
|
|
|
|
test.describe('Mask Editor load/save', { tag: '@vue-nodes' }, () => {
|
|
test('Save with drawn mask uploads non-empty mask data', async ({
|
|
comfyPage,
|
|
maskEditor
|
|
}) => {
|
|
const dialog = await maskEditor.openDialog()
|
|
await maskEditor.drawStrokeAndExpectPixels(dialog)
|
|
|
|
let observedContentType = ''
|
|
let observedBodyLength = 0
|
|
|
|
await comfyPage.page.route('**/upload/image', async (route) => {
|
|
const request = route.request()
|
|
if (!observedContentType) {
|
|
observedContentType = (await request.headerValue('content-type')) ?? ''
|
|
observedBodyLength = request.postDataBuffer()?.byteLength ?? 0
|
|
}
|
|
await route.fulfill(
|
|
fulfillJson(successResponse('clipspace-mask-123.png'))
|
|
)
|
|
})
|
|
|
|
await dialog.getByRole('button', { name: 'Save' }).click()
|
|
await expect(dialog).toBeHidden()
|
|
expect(observedContentType).toContain('multipart/form-data')
|
|
expect(observedBodyLength).toBeGreaterThan(256)
|
|
})
|
|
|
|
test('Canvas dimensions match the loaded image', async ({ maskEditor }) => {
|
|
const dialog = await maskEditor.openDialog()
|
|
|
|
const imageDimensions =
|
|
await maskEditor.getCanvasPixelData(IMAGE_CANVAS_INDEX)
|
|
const maskDimensions =
|
|
await maskEditor.getCanvasPixelData(MASK_CANVAS_INDEX)
|
|
|
|
expect(imageDimensions).not.toBeNull()
|
|
expect(maskDimensions).not.toBeNull()
|
|
expect(imageDimensions?.totalPixels).toBe(64 * 64)
|
|
expect(maskDimensions?.totalPixels).toBe(64 * 64)
|
|
|
|
await expect(dialog).toBeVisible()
|
|
})
|
|
|
|
test('Save failure keeps dialog open', async ({ comfyPage, maskEditor }) => {
|
|
const dialog = await maskEditor.openDialog()
|
|
await maskEditor.drawStrokeAndExpectPixels(dialog)
|
|
|
|
let imageUploadHit = false
|
|
await comfyPage.page.route('**/upload/image', (route) => {
|
|
imageUploadHit = true
|
|
return route.fulfill({ status: 500 })
|
|
})
|
|
|
|
const saveButton = dialog.getByRole('button', { name: 'Save' })
|
|
await saveButton.click()
|
|
|
|
await expect.poll(() => imageUploadHit).toBe(true)
|
|
await expect(dialog).toBeVisible()
|
|
await expect(saveButton).toBeVisible()
|
|
})
|
|
})
|