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>
90 lines
5.1 KiB
TypeScript
90 lines
5.1 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import { execFileSync } from "node:child_process";
|
|
import { expect, test } from "@playwright/test";
|
|
import { NativeUIWorkflow, observeNative } from "../fixtures/native-ui-workflow";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const fixture = "tests/files/web/basic_scene.blend";
|
|
const reader = "web/tests/fixtures/operator-bridge-main.py";
|
|
const hash = (value: Buffer | string) => crypto.createHash("sha256").update(value).digest("hex");
|
|
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
if (testInfo.status === testInfo.expectedStatus) return;
|
|
const button = page.getByTestId("export-diagnostics");
|
|
if (await button.isEnabled()) {
|
|
const download = page.waitForEvent("download");
|
|
await button.click();
|
|
await (await download).saveAs(testInfo.outputPath("diagnostics.json"));
|
|
}
|
|
});
|
|
|
|
test("F3 native operator properties, rejection and Main history", async ({ page }, testInfo) => {
|
|
test.setTimeout(90_000);
|
|
const receipts = process.env.WBV2_RECEIPT_DIR ?? testInfo.outputPath("receipts");
|
|
const directory = path.join(receipts, "operator-palette-artifacts");
|
|
await observeNative(page);
|
|
const flow = new NativeUIWorkflow(page, directory);
|
|
await page.goto("/");
|
|
await flow.load(path.join(root, fixture), "BasicCube");
|
|
await flow.save("before");
|
|
const dialog = page.getByRole("dialog", { name: "操作搜索", exact: true });
|
|
const choose = async () => {
|
|
await page.getByRole("button", { name: "操作搜索", exact: true }).click();
|
|
await dialog.getByRole("textbox", { name: "搜索 Blender 操作" }).fill("mesh.primitive_cube_add");
|
|
await dialog.getByRole("button").filter({ hasText: "[WEB_MAIN]" }).first().click();
|
|
await expect(dialog.locator("form")).toBeVisible();
|
|
};
|
|
const execute = async () => {
|
|
await choose();
|
|
await dialog.locator("form").getByLabel("name", { exact: true }).fill("BridgePrimitive");
|
|
await flow.click(dialog.getByRole("button", { name: "执行", exact: true }));
|
|
await expect(dialog).toBeHidden();
|
|
};
|
|
await choose();
|
|
await dialog.locator("form").getByLabel("name", { exact: true }).fill("x".repeat(64));
|
|
await flow.click(dialog.getByRole("button", { name: "执行", exact: true }), false);
|
|
await expect(dialog.getByRole("alert")).toBeVisible();
|
|
await dialog.getByRole("button", { name: "关闭操作搜索" }).click();
|
|
await flow.save("negative");
|
|
await execute();
|
|
await flow.save("after");
|
|
await flow.click(page.getByRole("button", { name: "撤销", exact: true }));
|
|
await flow.save("undo");
|
|
await flow.click(page.getByRole("button", { name: "重做", exact: true }));
|
|
await flow.save("redo");
|
|
await flow.load(path.join(directory, "redo.blend"), "BridgePrimitive");
|
|
await flow.save("reopened");
|
|
// Reloading the page creates a fresh worker, rather than reusing the first engine.
|
|
await page.reload();
|
|
await flow.load(path.join(root, fixture), "BasicCube");
|
|
await execute();
|
|
await flow.save("second");
|
|
await page.screenshot({ path: path.join(directory, "palette.png") });
|
|
fs.writeFileSync(path.join(directory, "primitive.json"), JSON.stringify({ primitive: "cube" }));
|
|
const binary = path.resolve(root, process.env.BLENDER_BIN ?? "build_blender_native/bin/blender");
|
|
const log = fs.openSync(path.join(directory, "desktop.log"), "w");
|
|
try { execFileSync(binary, ["--background", "--factory-startup", "--python-exit-code", "1", "--python", path.join(root, reader), "--", directory], { timeout: 30_000, stdio: ["ignore", log, log] }); }
|
|
finally { fs.closeSync(log); }
|
|
const states = JSON.parse(fs.readFileSync(path.join(directory, "native.json"), "utf8"));
|
|
expect(states.after).toEqual(states.expected);
|
|
expect(states.after).not.toEqual(states.before);
|
|
expect(states.negative).toEqual(states.before);
|
|
expect(states.undo).toEqual(states.before);
|
|
for (const stage of ["redo", "reopened", "second"]) expect(states[stage]).toEqual(states.after);
|
|
const digest = (stage: string) => hash(JSON.stringify(states[stage]));
|
|
const observations = {
|
|
positive: { before: digest("before"), after: digest("after") },
|
|
negative: { before: digest("before"), after: digest("negative") },
|
|
undo_redo: { before: digest("before"), after: digest("after"), undo: digest("undo"), redo: digest("redo") },
|
|
save_reopen: { saved: digest("redo"), reopened: digest("reopened") },
|
|
determinism: { first: digest("after"), second: digest("second") },
|
|
};
|
|
const artifacts = [fixture, reader, "web/tests/e2e/operator-palette-native.spec.ts", path.relative(root, binary), "web/app/public/vendor/blender/web_engine.wasm", ...fs.readdirSync(directory).map(name => path.relative(root, path.join(directory, name)))].map(file => ({ path: file, sha256: hash(fs.readFileSync(path.join(root, file))) }));
|
|
for (const [kind, values] of Object.entries(observations)) {
|
|
const caseId = `operator-palette-${kind}`;
|
|
fs.writeFileSync(path.join(receipts, `${caseId}.json`), JSON.stringify({ status: "PASS", runtime: "CHROMIUM_WEBENGINE", caseId, kind, unitIds: ["web-command:createPrimitive", "operator:WM_OT_open_mainfile"], observations: values, artifacts }, null, 2) + "\n");
|
|
}
|
|
});
|