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>
80 lines
5.1 KiB
TypeScript
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));
|
|
}
|
|
}
|
|
});
|