fix(POSTV1-editor): 修复多选拖动与底图调整
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-06 10:53:38 +08:00
parent 51d613f459
commit 83fe57f319
8 changed files with 310 additions and 14 deletions
@@ -67,6 +67,20 @@ async function routeEditor(page: Page) {
});
}
async function canvasFingerprint(page: Page) {
return page.getByLabel("编辑画布").evaluate((canvas: HTMLCanvasElement) => {
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas context unavailable.");
return [
...context.getImageData(108, 720, 1, 1).data,
...context.getImageData(324, 720, 1, 1).data,
...context.getImageData(540, 720, 1, 1).data,
...context.getImageData(756, 720, 1, 1).data,
...context.getImageData(972, 720, 1, 1).data,
];
});
}
test("TDD-WP4-BG-001 preserves overlays while switching the background", async ({ page }) => {
await routeEditor(page);
const saves: Array<Record<string, unknown>> = [];
@@ -144,3 +158,35 @@ test("TDD-WP4-BG-002 commits, reopens, undoes, and resets background processing"
writeEvidence("TDD-WP4-BG-002-processing-controls", "pixel-diff.json", { preview_commit_undo_reset: true, export_source_canvas_state_stable: true });
await page.screenshot({ fullPage: true, path: process.env.DADA_EVIDENCE_DIR_EDITOR ? resolve(process.env.DADA_EVIDENCE_DIR_EDITOR, "TDD-WP4-BG-002-processing-controls", "processing-controls.png") : undefined });
});
test("TDD-WP4-BG-002 previews background pixels before committing", async ({ page }) => {
await routeEditor(page);
const saves: Array<Record<string, unknown>> = [];
let version = 7;
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
saves.push(route.request().postDataJSON() as Record<string, unknown>);
version += 1;
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: version }), contentType: "application/json", status: 200 });
});
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
await page.getByRole("button", { name: "恢复原图" }).click();
await page.getByRole("button", { name: "应用调整" }).click();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(0);
await expect.poll(async () => (await canvasFingerprint(page)).some((channel) => channel < 240)).toBe(true);
const baseline = (await canvasFingerprint(page)).join(",");
const ranges = page.locator(".editor-inspector input[type=range]");
await ranges.nth(0).fill("40");
await ranges.nth(1).fill("25");
await ranges.nth(2).fill("-35");
await ranges.nth(3).fill("70");
await ranges.nth(4).fill("100");
await expect.poll(async () => (await canvasFingerprint(page)).join(",")).not.toBe(baseline);
await page.getByRole("button", { name: "应用调整" }).click();
await expect.poll(() => saves.length, { timeout: 4_000 }).toBeGreaterThan(1);
const committed = saves.at(-1) as { canvas_state: typeof initialCanvasState };
expect(committed.canvas_state.background.adjustments).toMatchObject({
brightness: 40, contrast: 25, saturation: -35, sharpness: 100, temperature: 70,
});
});
+37 -1
View File
@@ -28,7 +28,8 @@ const session = {
user: { creator_name: "Canvas User", role: "user", social_id: "@canvas_user", status: "active", user_id: "00000000-0000-4000-8000-000000000501" },
};
const stickerRoot = process.env.DADA_STATIC_STICKER_ROOT ?? join(homedir(), "Desktop", "贴纸素材");
const stickerRoot = process.env.DADA_STATIC_STICKER_ROOT
?? join(homedir(), "Desktop", "sticker_web_replication_assets", "sticker_normal");
const originalStickerFixtures: Readonly<Record<string, string>> = {
STK001: join(stickerRoot, "sticker_part1", "01af6384c2a962d17f55736f9895b505.png"),
STK002: join(stickerRoot, "sticker_part2", "011db0fb6ac4184e4a708374003bce66.png"),
@@ -133,6 +134,41 @@ test("TDD-WP4-CAN-001 keeps fifty elements editable and blocks the fifty-first",
if (process.env.DADA_EVIDENCE_DIR_EDITOR_ELEMENTS) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_EDITOR_ELEMENTS, "TDD-WP4-CAN-001-fifty-elements", "fifty-elements.png") });
});
test("TDD-WP4-CAN-001 drags distant multi-selected objects as one group", async ({ page }) => {
const projectId = uuid(519);
const backend = {
canvas: canvas([sticker(1, { x: 0.2, y: 0.2 }, 0), sticker(2, { x: 0.8, y: 0.8 }, 1)]),
saves: 0,
version: 5,
};
await routeEditor(page, projectId, backend);
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
const stage = page.getByLabel("编辑画布");
const bounds = await stage.boundingBox();
if (!bounds) throw new Error("Canvas bounds unavailable.");
const point = (x: number, y: number) => ({ x: bounds.x + bounds.width * x, y: bounds.y + bounds.height * y });
await page.getByRole("button", { name: "多选模式" }).click();
await page.mouse.click(point(0.2, 0.2).x, point(0.2, 0.2).y);
await page.mouse.click(point(0.8, 0.8).x, point(0.8, 0.8).y);
await expect(page.getByRole("heading", { name: "已选 2 个对象" })).toBeVisible();
await page.mouse.move(point(0.2, 0.2).x, point(0.2, 0.2).y);
await page.mouse.down();
await page.mouse.move(point(0.3, 0.3).x, point(0.3, 0.3).y, { steps: 4 });
await page.mouse.up();
await expect.poll(() => backend.saves, { timeout: 4_000 }).toBeGreaterThan(0);
const [first, second] = backend.canvas.elements.map(({ position }) => position);
expect(first?.x).toBeCloseTo(0.3, 6);
expect(first?.y).toBeCloseTo(0.3, 6);
expect(second?.x).toBeCloseTo(0.9, 6);
expect(second?.y).toBeCloseTo(0.9, 6);
expect((first?.x ?? 0) - 0.2).toBeCloseTo((second?.x ?? 0) - 0.8, 10);
expect((first?.y ?? 0) - 0.2).toBeCloseTo((second?.y ?? 0) - 0.8, 10);
await expect(page.getByRole("heading", { name: "已选 2 个对象" })).toBeVisible();
});
test("TDD-WP4-STK-001 transforms, cycles, selects and reopens ordinary stickers", async ({ page }) => {
const projectId = uuid(520);
const backend = { canvas: canvas([sticker(1, { x: 0.5, y: 0.5 }, 0), sticker(2, { x: 0.5, y: 0.5 }, 1)]), saves: 0, version: 5 };
+32
View File
@@ -1,8 +1,11 @@
import { describe, expect, it } from "vitest";
import {
adjustBackgroundPixels,
backgroundDrawPlan,
CanvasEditHistory,
createEditorCanvasState,
cssFilterForBackground,
defaultBackgroundAdjustments,
deserializeFabricCanvas,
switchBackground,
@@ -24,6 +27,35 @@ const element = {
};
describe("TDD-WP4-BG-001/002 canvas boundary", () => {
it("builds valid filters and distinct contain/crop draw plans", () => {
const adjustments = { ...defaultBackgroundAdjustments(), brightness: 25, contrast: 15, saturation: -20 };
expect(cssFilterForBackground(adjustments)).toBe("brightness(125%) contrast(115%) saturate(80%)");
expect(cssFilterForBackground(defaultBackgroundAdjustments())).toBe("none");
expect(backgroundDrawPlan({ height: 200, width: 400 }, { height: 100, width: 100 }, { ...adjustments, fit: "fit" })).toEqual({
destination: { height: 50, width: 100, x: 0, y: 25 },
source: { height: 200, width: 400, x: 0, y: 0 },
});
expect(backgroundDrawPlan({ height: 200, width: 400 }, { height: 100, width: 100 }, { ...adjustments, fit: "crop" })).toEqual({
destination: { height: 100, width: 100, x: 0, y: 0 },
source: { height: 200, width: 200, x: 100, y: 0 },
});
});
it("changes pixels for temperature and sharpness without changing alpha", () => {
const flat = new Uint8ClampedArray([100, 100, 100, 255]);
expect([...adjustBackgroundPixels(flat, 1, 1, { sharpness: 0, temperature: 100 })]).toEqual([135, 108, 65, 255]);
const edged = new Uint8ClampedArray([
100, 100, 100, 255, 100, 100, 100, 255, 100, 100, 100, 255,
100, 100, 100, 255, 180, 180, 180, 255, 100, 100, 100, 255,
100, 100, 100, 255, 100, 100, 100, 255, 100, 100, 100, 255,
]);
const sharpened = adjustBackgroundPixels(edged, 3, 3, { sharpness: 100, temperature: 0 });
expect(sharpened[16]).toBe(255);
expect(sharpened[19]).toBe(255);
});
it("switches background without moving overlays, resets processing, and re-extracts palette", () => {
const initial = createEditorCanvasState({ assetId: "00000000-0000-4000-8000-000000000010", ratio: "3:4" });
const withOverlay = { ...initial, background: { ...initial.background, adjustments: { ...initial.background.adjustments, brightness: 42, filter: "mono" } }, elements: [element] };
+17
View File
@@ -73,6 +73,10 @@ describe("TDD-WP4-CAN-001 canvas selection and limit", () => {
identity(1).elementId,
identity(2).elementId,
]);
expect(controller.selectAt({ x: 0.2, y: 0.2 }, { append: true, preserveSelection: true })).toEqual([
identity(1).elementId,
identity(2).elementId,
]);
});
});
@@ -97,6 +101,19 @@ describe("TDD-WP4-STK-001 sticker transformations", () => {
});
});
it("uses one bounded delta for distant selected elements", () => {
const controller = new CanvasElementController(state([
sticker(1, { position: { x: 0.1, y: 0.2 } }),
sticker(2, { position: { x: 0.9, y: 0.8 } }),
]));
controller.selectIds([identity(1).elementId, identity(2).elementId]);
controller.moveSelected({ x: 0.2, y: 0.3 }, { snap: false });
expect(controller.value.elements.map(({ position }) => position)).toEqual([
{ x: 0.2, y: 0.4 },
{ x: 1, y: 1 },
]);
});
it("duplicates, reorders and deletes selected stickers while preserving stable resource identity", () => {
const controller = new CanvasElementController(state([sticker(1), sticker(2, { position: { x: 0.7, y: 0.7 } })]));
controller.selectById(identity(1).elementId);