import { test, expect } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; test("F3 loads native RNA properties on demand and submits a typed Empty operation", async ({ page }) => { const root = path.resolve(".."); const output = process.env.WBV2_RECEIPT_DIR ? path.join(process.env.WBV2_RECEIPT_DIR, "schema-palette-ui") : path.join(root, "tests/golden/WBV2/autopilot/schema-palette-ui"); fs.mkdirSync(output, { recursive: true }); let descriptions = 0; let submissions = 0; page.on("request", (request) => { if (!request.url().includes("server-operator-job")) return; const action = request.headers()["x-server-operator-action"]; if (action === "describe") descriptions += 1; if (action === "submit") submissions += 1; }); await page.goto("/"); const fileInput = page.getByTestId("blend-file-input"); await expect(fileInput).toBeEnabled(); await fileInput.setInputFiles(path.join(root, "tests/files/web/generated/WBV2-P24-001-OBJECT_OPERATIONS.MOVE_COLLECTION.blend")); await expect(page.getByRole("treeitem")).toHaveCount(4); expect(descriptions).toBe(0); await page.keyboard.press("F3"); const dialog = page.getByRole("dialog"); await dialog.getByLabel("搜索 Blender 操作", { exact: true }).fill("OBJECT_OT_add"); const [description] = await Promise.all([ page.waitForResponse((response) => response.url().includes("server-operator-job") && response.request().headers()["x-server-operator-action"] === "describe"), dialog.getByRole("button").filter({ hasText: /^OBJECT_OT_add \[SERVER_NATIVE\]/ }).click(), ]); expect(description.status()).toBe(200); const schema = (await description.json()).result; expect(schema.route).toBe("SERVER_NATIVE"); const typeProperty = schema.operator.properties.find((property: { name: string }) => property.name === "type"); expect(typeProperty.type).toBe("enum"); const typeInput = dialog.locator("label").filter({ hasText: /^Type/ }).locator("select"); await expect(typeInput.locator("option")).toHaveCount(typeProperty.enumItems.length); await typeInput.selectOption({ label: "Empty" }); const radiusProperty = schema.operator.properties.find((property: { name: string }) => property.name === "radius"); const radius = dialog.getByLabel(radiusProperty.label ?? "radius", { exact: true }); await radius.fill("2.5"); const locationProperty = schema.operator.properties.find((property: { name: string }) => property.name === "location"); expect(locationProperty.type).toBe("number-array"); expect(locationProperty.arrayLength).toBe(3); await dialog.getByLabel(`${locationProperty.label} X`, { exact: true }).fill("1.25"); const editMode = schema.operator.properties.find((property: { name: string }) => property.name === "enter_editmode"); await expect(dialog.getByLabel(editMode.label ?? "enter_editmode", { exact: true })).not.toBeChecked(); const [submission] = await Promise.all([ page.waitForResponse((response) => response.url().includes("server-operator-job") && response.request().headers()["x-server-operator-action"] === "submit"), dialog.locator('button[type="submit"]').click(), ]); expect(submission.status()).toBe(202); const job = (await submission.json()).result; const defaults = Object.fromEntries(schema.operator.properties.filter((property: { defaultValue?: unknown }) => property.defaultValue !== undefined && property.defaultValue !== "").map((property: { name: string; defaultValue: unknown }) => [property.name, property.defaultValue])); const properties = { ...defaults, align: "WORLD", enter_editmode: false, radius: 2.5, type: "EMPTY", location: [1.25, 0, 0] }; const canonical = JSON.stringify(Object.fromEntries(Object.entries(properties).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))); const propertiesSha256 = crypto.createHash("sha256").update(canonical).digest("hex"); expect(job.propertiesSha256).toBe(propertiesSha256); await expect(dialog).not.toBeVisible(); await expect(page.getByRole("treeitem")).toHaveCount(5); await expect(page.getByText("Empty", { exact: true })).toBeVisible(); expect(descriptions).toBe(1); expect(submissions).toBe(1); const screenshot = path.join(output, "f3-empty-result.png"); await page.screenshot({ path: screenshot }); const evidence = path.join(output, "ui-result.json"); fs.writeFileSync(evidence, JSON.stringify({ status: "PASS", runtime: "CHROMIUM_WEBENGINE", operator: "OBJECT_OT_add", descriptions, submissions, beforeTreeItems: 4, afterTreeItems: 5, propertiesSha256, nativeSchema: schema }, null, 2)); const artifacts = [screenshot, evidence].map((file) => ({ path: path.relative(root, file), sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") })); if (process.env.WBV2_RECEIPT_DIR) { for (const kind of ["positive", "negative", "undo_redo", "save_reopen", "determinism"]) { const file = path.join(process.env.WBV2_RECEIPT_DIR, `server-http-${kind}.json`); const receipt = JSON.parse(fs.readFileSync(file, "utf8")); receipt.artifacts.push(...artifacts); fs.writeFileSync(file, JSON.stringify(receipt, null, 2)); } } });