fix(POSTV1-08): 修复文字拖动闪烁与自动保存
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-05 18:47:11 +08:00
parent 468bb5579d
commit e4aec01ea6
4 changed files with 244 additions and 36 deletions
+88
View File
@@ -5,6 +5,8 @@ import { homedir } from "node:os";
import { join, resolve } from "node:path";
import type { CanvasState } from "@dada/shared-contracts";
import { P0A_TEXT_TEMPLATES, createTextTemplateElement } from "../../apps/web/src/text-assets.js";
let vite: ViteDevServer;
let webUrl: string;
@@ -196,3 +198,89 @@ test("TDD-WP4-STK-001 transforms, cycles, selects and reopens ordinary stickers"
writeEvidence("TDD-WP4-STK-001-transform-sticker", "pixel-diff.json", { canvas_and_saved_state_match: true, export_source_canvas_state_stable: true });
if (process.env.DADA_EVIDENCE_DIR_EDITOR_ELEMENTS) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_EDITOR_ELEMENTS, "TDD-WP4-STK-001-transform-sticker", "transformed-sticker.png") });
});
test("POSTV1-08 keeps the canvas frame stable and previews drag before pointer release", async ({ page }) => {
await page.addInitScript(() => {
const counters = { height: 0, width: 0 };
Object.defineProperty(window, "__dadaCanvasDimensionWrites", { value: counters });
for (const key of ["height", "width"] as const) {
const descriptor = Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, key);
if (!descriptor?.get || !descriptor.set) throw new Error(`Canvas ${key} descriptor unavailable.`);
Object.defineProperty(HTMLCanvasElement.prototype, key, {
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
get: descriptor.get,
set(value: number) {
if (this.classList.contains("editor-canvas")) counters[key] += 1;
descriptor.set!.call(this, value);
},
});
}
});
const projectId = uuid(530);
const text = createTextTemplateElement(P0A_TEXT_TEMPLATES[0]!, {
createdAt: "2026-08-03T08:00:00.000Z",
elementId: uuid(630),
}, 0, { position: { x: 0.5, y: 0.5 } });
const backend = { canvas: canvas([text]), saves: 0, version: 6 };
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 center = { x: bounds.x + bounds.width * 0.5, y: bounds.y + bounds.height * 0.5 };
await page.mouse.click(center.x, center.y);
await expect(page.getByLabel("文字内容")).toBeVisible();
await page.getByLabel("文字内容").fill("拖动中的文字");
await page.getByRole("spinbutton", { name: "有效字号", exact: true }).fill("64");
await page.getByLabel("文字填充色").fill("#FA5751");
await expect.poll(() => backend.saves, { timeout: 4_000 }).toBe(1);
expect(backend.canvas.elements[0]).toMatchObject({
content: "拖动中的文字",
scale: { x: 64 / 48, y: 64 / 48 },
style_parameters: { fill_color: "#FA5751" },
});
const savesBeforeDrag = backend.saves;
const before = await page.evaluate(() => {
return structuredClone((window as typeof window & { __dadaCanvasDimensionWrites: { height: number; width: number } }).__dadaCanvasDimensionWrites);
});
await page.mouse.move(center.x, center.y);
await page.mouse.down();
await page.waitForTimeout(650);
await expect(page.getByRole("menu")).toBeVisible();
await page.mouse.move(bounds.x + bounds.width * 0.68, center.y);
await expect(page.getByRole("menu")).toBeHidden();
await expect(page.getByLabel("文字内容")).toBeVisible();
const preview = await stage.evaluate((canvas: HTMLCanvasElement) => {
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas context unavailable.");
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
let count = 0;
let totalX = 0;
for (let y = 0; y < canvas.height; y += 1) {
for (let x = 0; x < canvas.width; x += 1) {
const offset = (y * canvas.width + x) * 4;
if ((pixels[offset] ?? 255) < 20 && (pixels[offset + 1] ?? 0) >= 75 && (pixels[offset + 1] ?? 255) <= 120 && (pixels[offset + 2] ?? 0) >= 180) {
count += 1;
totalX += x;
}
}
}
return { blue_pixel_count: count, blue_x: count > 0 ? totalX / count / canvas.width : 0 };
});
const during = await page.evaluate(() => {
return structuredClone((window as typeof window & { __dadaCanvasDimensionWrites: { height: number; width: number } }).__dadaCanvasDimensionWrites);
});
expect(during).toEqual(before);
expect(preview.blue_pixel_count).toBeGreaterThan(100);
expect(preview.blue_x).toBeGreaterThan(0.60);
await page.mouse.up();
await expect.poll(() => backend.saves, { timeout: 4_000 }).toBeGreaterThan(savesBeforeDrag);
expect(backend.canvas.elements[0]?.position.x).toBeCloseTo(0.68, 2);
expect(backend.canvas.elements[0]?.content).toBe("拖动中的文字");
await expect(page.getByLabel("文字内容")).toBeVisible();
});
+14 -11
View File
@@ -78,7 +78,7 @@ async function routeEditor(page: Page, projectId: string, backend: Backend, opti
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
}
test("TDD-WP4-EXP-001 cancel keeps a pending text edit outside history and export", async ({ page }) => {
test("TDD-WP4-EXP-001 cancel keeps automatically saved text outside the export", async ({ page }) => {
const projectId = "00000000-0000-4000-8000-000000000920";
const assetId = "00000000-0000-4000-8000-000000000921";
const backend: Backend = { canvas: canvasForRatio("3:4", assetId), latestBodies: [], saves: 0, version: 2 };
@@ -87,27 +87,29 @@ test("TDD-WP4-EXP-001 cancel keeps a pending text edit outside history and expor
await page.getByRole("button", { name: "文字模板", exact: true }).click();
await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(1);
await page.getByLabel("文字内容").fill("尚未提交的导出文字");
await page.getByLabel("文字内容").fill("自动保存的导出文字");
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(2);
const downloads: string[] = [];
page.on("download", (download) => downloads.push(download.suggestedFilename()));
await page.getByRole("button", { name: "导出", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "导出成品" });
await expect(dialog).toContainText("导出前需要提交当前修改");
await expect(dialog.getByText("将应用当前修改并导出", { exact: true })).toBeVisible();
await expect(dialog).not.toContainText("导出前需要提交当前修改");
await expect(dialog.getByRole("checkbox", { name: "将应用当前修改并导出" })).toHaveCount(0);
await dialog.getByRole("button", { name: "取消" }).click();
await expect(dialog).toHaveCount(0);
await expect(page.getByLabel("文字内容")).toHaveValue("尚未提交的导出文字");
expect(backend.saves).toBe(1);
await expect(page.getByLabel("文字内容")).toHaveValue("自动保存的导出文字");
expect(backend.saves).toBe(2);
expect(backend.canvas.elements[0]?.content).toBe("自动保存的导出文字");
expect(backend.latestBodies).toHaveLength(0);
expect(downloads).toHaveLength(0);
const beforeUndo = { download_count: 0, latest_count: 0, save_count_after_cancel: backend.saves, state_version: backend.version };
await page.getByRole("button", { name: "撤销" }).click();
await expect(page.getByLabel("文字内容")).toHaveCount(0);
await expect(page.getByLabel("文字内容")).toHaveValue("春日计划");
evidence("TDD-WP4-EXP-001-cancel-pending-edit", "network-timeline.json", { ...beforeUndo, compose_calls: 0, export_save_calls: 0 });
evidence("TDD-WP4-EXP-001-cancel-pending-edit", "db-diff.json", { committed_text_after_cancel: "春日计划", first_undo_removed_initial_element: true, latest_exports_changed: false });
evidence("TDD-WP4-EXP-001-cancel-pending-edit", "db-diff.json", { committed_text_after_cancel: "自动保存的导出文字", first_undo_restored_initial_text: true, latest_exports_changed: false });
});
test("TDD-WP4-EXP-001 confirm commits once, downloads, and saves the same bytes", async ({ page }) => {
test("TDD-WP4-EXP-001 exports automatically saved text and saves the same bytes", async ({ page }) => {
const projectId = "00000000-0000-4000-8000-000000000930";
const assetId = "00000000-0000-4000-8000-000000000931";
const backend: Backend = { canvas: canvasForRatio("3:4", assetId), latestBodies: [], saves: 0, version: 4 };
@@ -117,6 +119,7 @@ test("TDD-WP4-EXP-001 confirm commits once, downloads, and saves the same bytes"
await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(1);
await page.getByLabel("文字内容").fill("确认后进入导出");
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(2);
await page.getByRole("button", { name: "导出", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "导出成品" });
if (process.env.DADA_EVIDENCE_DIR_EXPORT) {
@@ -124,14 +127,14 @@ test("TDD-WP4-EXP-001 confirm commits once, downloads, and saves the same bytes"
mkdirSync(dirname(screenshot), { recursive: true });
await page.screenshot({ fullPage: true, path: screenshot });
}
await dialog.getByRole("checkbox", { name: "将应用当前修改并导出" }).check();
await expect(dialog.getByRole("checkbox", { name: "将应用当前修改并导出" })).toHaveCount(0);
const downloadPromise = page.waitForEvent("download");
await dialog.getByRole("button", { name: "导出并下载" }).click();
const download = await downloadPromise;
const downloadPath = await download.path();
if (!downloadPath) throw new Error("Browser download did not expose a local path.");
await expect(dialog.getByRole("status")).toHaveText("已下载并保存为最新成品");
await expect.poll(() => backend.saves).toBe(2);
expect(backend.saves).toBe(2);
expect(backend.canvas.elements[0]?.content).toBe("确认后进入导出");
expect(backend.latestBodies).toHaveLength(1);
const downloaded = readFileSync(downloadPath);