164 lines
7.4 KiB
TypeScript
164 lines
7.4 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { hasScaleAdjustTargetChanged } from "../../components/canvas/nodes/image-node/image-node-local-edit";
|
|
|
|
function readSource(path: string) {
|
|
return readFileSync(resolve(process.cwd(), path), "utf8");
|
|
}
|
|
|
|
test.describe("canvas scale adjust product lock", () => {
|
|
test("accepts target movement even when target size stays unchanged", () => {
|
|
const source = { height: 0.4, width: 0.3, x: 0.2, y: 0.25 };
|
|
|
|
expect(hasScaleAdjustTargetChanged(source, source)).toBe(false);
|
|
expect(
|
|
hasScaleAdjustTargetChanged(source, { ...source, x: 0.46, y: 0.12 })
|
|
).toBe(true);
|
|
expect(
|
|
hasScaleAdjustTargetChanged(source, {
|
|
...source,
|
|
height: 0.32,
|
|
width: 0.24,
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
test("uses the same movement validation in canvas and detail result flows", () => {
|
|
const canvasNode = readSource("components/canvas/nodes/ImageNode.tsx");
|
|
const detailWorkspace = readSource(
|
|
"components/agents/AgentGenerationWorkspaceDarkV2.tsx"
|
|
);
|
|
|
|
expect(canvasNode).toContain("hasScaleAdjustTargetChanged(");
|
|
expect(detailWorkspace).toContain("hasScaleAdjustTargetChanged(");
|
|
expect(canvasNode).toContain("青色目标框大小或位置");
|
|
expect(detailWorkspace).toContain("目标框大小和位置均未变化");
|
|
});
|
|
|
|
test("uses one dedicated erase step and exact product overlay", () => {
|
|
const route = readSource("app/api/canvas/local-edit/route.ts");
|
|
|
|
expect(route).toContain("buildScaleAdjustProductLockAssets");
|
|
expect(route).toContain('objectRemovalMode: "premium"');
|
|
expect(route).toContain("objectRemovalMaskImageUrl: modelMaskUpload.url");
|
|
expect(route).toContain("maskImageUrl: modelMaskUpload.url");
|
|
expect(route).toContain("hasPositionMove");
|
|
expect(route).toContain('aspectRatio: hasPositionMove ? "auto" : resolvedAspectRatio');
|
|
expect(route).toContain("overlayImageUrl: overlayUpload.url");
|
|
expect(route).toContain("repairMaskImageUrl: repairMaskUpload.url");
|
|
expect(route).toContain("sourceImageUrl");
|
|
expect(route).toContain("protectPeople: hasPositionMove");
|
|
expect(route).toContain("oldFootprintMaskImageUrl: hasPositionMove");
|
|
expect(route).toContain("? sourceSubjectMaskUpload?.url");
|
|
expect(route).toContain("deterministicProductOverlayApplied: true");
|
|
expect(route).toContain("scaleAdjustPersonExclusionApplied:");
|
|
expect(route).toContain("sourceSubjectMaskImageUrl: sourceSubjectMaskUpload.url");
|
|
expect(route).toContain("shadowSourceImageUrl: sourceImageUrl");
|
|
expect(route).toContain('"finegrain-erase-product-lock-v1"');
|
|
expect(route).toContain('"gpt-mask-background-rebuild-product-lock-v1"');
|
|
expect(route).not.toContain("buildScaleAdjustVisualGuide");
|
|
expect(route).not.toContain("source-plus-guide-v1");
|
|
});
|
|
|
|
test("routes object removal through the configured fal credential", () => {
|
|
const provider = readSource("lib/ai/providers/fal-image.ts");
|
|
|
|
expect(provider).toContain(
|
|
'const FAL_OBJECT_REMOVAL_MODEL_ID = "fal-ai/finegrain-eraser/mask"'
|
|
);
|
|
expect(provider).toContain("image_url: imageUrls[0]");
|
|
expect(provider).toContain("mask_url: objectRemovalMaskImageUrl");
|
|
expect(provider).toContain('objectRemovalMode || ""');
|
|
});
|
|
|
|
test("retries old-footprint failures against the first cleaned result", () => {
|
|
const route = readSource("app/api/canvas/local-edit/route.ts");
|
|
const generator = readSource("lib/canvas/generate-image.ts");
|
|
|
|
expect(route).toContain("rejectOldFootprintOnly: true");
|
|
expect(route).toContain("scaleAdjustSequentialCleanupRetry: true");
|
|
expect(route).toContain('label: "finegrain-cleanup-pass"');
|
|
expect(generator).toContain(
|
|
'scaleAdjustRejection?.reason === "old-footprint-remnant-detected"'
|
|
);
|
|
expect(generator).toContain("referenceImages: [sequentialCleanupReferenceUrl]");
|
|
});
|
|
|
|
test("keeps transparent product images on a deterministic empty plate", () => {
|
|
const productLock = readSource("lib/canvas/scale-adjust-product-lock.ts");
|
|
|
|
expect(productLock).toContain("hasTransparentBackground");
|
|
expect(productLock).toContain("sharp(transparentCanvas)");
|
|
expect(productLock).toContain("backgroundPlateBuffer");
|
|
});
|
|
|
|
test("protects people and rejects products materially occluded by them", () => {
|
|
const productLock = readSource("lib/canvas/scale-adjust-product-lock.ts");
|
|
const rmbg = readSource("lib/ai/rmbg/service.ts");
|
|
|
|
expect(productLock).toContain('foregroundType: "person"');
|
|
expect(productLock).toContain("regionPersonExclusionAlpha");
|
|
expect(productLock).toContain("overlapPixelCount >= overlapLimit");
|
|
expect(productLock).toContain("目标产品被人物遮挡");
|
|
expect(productLock).toContain("personExclusionApplied: Boolean(personExclusionAlpha)");
|
|
expect(productLock).toContain("expandedAlphaRaw[index] >= 8 ? 255 : 0");
|
|
expect(productLock).toContain(".threshold(8)\n .extractChannel(0)");
|
|
expect(rmbg).toContain("foregroundType: foregroundType ??");
|
|
});
|
|
|
|
test("moves the original contact shadow with the exact product overlay", () => {
|
|
const generator = readSource("lib/canvas/generate-image.ts");
|
|
|
|
expect(generator).toContain("buildScaleAdjustTransferredShadow");
|
|
expect(generator).toContain("cleanedLuma - sourceLuma - 4");
|
|
expect(generator).toContain("transferredShadowBuffer");
|
|
expect(generator).toContain("transferredShadowCount");
|
|
expect(generator).toContain("if (!(isShrink || isMove)) continue");
|
|
expect(generator).toContain(
|
|
"const maskHit = explicitMaskRaw\n ? explicitMaskValue > 18"
|
|
);
|
|
expect(generator).toContain("(!isMove &&");
|
|
expect(generator).toContain(
|
|
"const generatedWidth = compositeBaseBuffer ? overlayWidth : modelWidth"
|
|
);
|
|
expect(generator).toContain(
|
|
".extractChannel(3)\n .blur(Math.max(4"
|
|
);
|
|
expect(generator).toContain(".extractChannel(0)\n .raw()\n .toBuffer()");
|
|
});
|
|
|
|
test("uses the image edit model to rebuild lighting at the target position", () => {
|
|
const route = readSource("app/api/canvas/local-edit/route.ts");
|
|
const queue = readSource("lib/canvas/image-task-queue.ts");
|
|
const relight = readSource("lib/canvas/scale-adjust-model-relight.ts");
|
|
|
|
expect(route).toContain("scaleAdjustModelRelight:");
|
|
expect(route).toContain("...(hasPositionMove");
|
|
expect(route).toContain("overlayImageUrl: overlayUpload.url");
|
|
expect(queue).toContain("applyScaleAdjustModelRelight");
|
|
expect(relight).toContain('debugLabel: "canvas-scale-adjust-model-relight"');
|
|
expect(relight).toContain("maskImageUrl: maskUpload.url");
|
|
expect(relight).toContain("生成真实自然的接触阴影与投射阴影");
|
|
expect(relight).toContain("findMedianLumaDelta");
|
|
expect(relight).toContain("productAlpha[pixelIndex] >= 8 ? 0");
|
|
});
|
|
|
|
test("keeps non-pro detail result scale adjust on the shared backend flow", () => {
|
|
const source = readSource(
|
|
"components/agents/AgentGenerationWorkspaceDarkV2.tsx"
|
|
);
|
|
|
|
expect(source).toContain(
|
|
"const resolvedLocalEditModelId = isScaleAdjust\n ? undefined"
|
|
);
|
|
expect(source).toContain(
|
|
'operation: isScaleAdjust ? "scale-adjust" : undefined'
|
|
);
|
|
expect(source).toContain("targetRect: region.targetRect");
|
|
expect(source).toContain('resultModeType: "detail_generation"');
|
|
expect(source).toContain('mode: "scale_adjust"');
|
|
});
|
|
});
|