Files
workinf_Blender_Wasm/web/tests/e2e/server-operator-schema-palette.spec.ts
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

80 lines
5.1 KiB
TypeScript

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));
}
}
});