first commit
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
PACKAGE_ROOT,
|
||||
RENDER_HTML_PATH,
|
||||
SNAPSHOT_ORIGIN,
|
||||
SnapshotError,
|
||||
chromiumLaunchOptions,
|
||||
loadJobFromOptions,
|
||||
parseSnapshotArgs,
|
||||
printRenderResult,
|
||||
resolveRenderJobPacket,
|
||||
resolveSnapshotRouteFile,
|
||||
timestampOutputPath,
|
||||
} from "./snapshot.mjs";
|
||||
|
||||
const ttyStdin = { isTTY: true };
|
||||
|
||||
function withTempImplicitModel(callback) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "implicitjs-snapshot-"));
|
||||
try {
|
||||
const models = path.join(root, "models", "implicit-cad");
|
||||
fs.mkdirSync(models, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(models, "orb.implicit.js"),
|
||||
"export default {glsl: 'float sdf(vec3 p) { return length(p) - 1.0; }'};\n",
|
||||
"utf8"
|
||||
);
|
||||
return callback({ root, models });
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("shortcut job shape is built by the JavaScript snapshot CLI", async () => {
|
||||
const options = parseSnapshotArgs([
|
||||
"--input",
|
||||
"models/implicit-cad/orb.implicit.js",
|
||||
"--output",
|
||||
"tmp/orb.png",
|
||||
"--camera",
|
||||
"front",
|
||||
"--width",
|
||||
"640",
|
||||
"--height",
|
||||
"480",
|
||||
"--params",
|
||||
"{\"radius\":24}",
|
||||
"--appearance",
|
||||
"workbench",
|
||||
"--graphics",
|
||||
"{\"detail\":2,\"resolutionScale\":3}",
|
||||
"--size-profile",
|
||||
"simple",
|
||||
]);
|
||||
|
||||
const job = await loadJobFromOptions(options, { stdin: ttyStdin, cwd: process.cwd() });
|
||||
|
||||
assert.equal(job.input, "models/implicit-cad/orb.implicit.js");
|
||||
assert.equal(job.mode, "view");
|
||||
assert.equal(job.outputs[0].path, "tmp/orb.png");
|
||||
assert.equal(job.outputs[0].camera, "front");
|
||||
assert.equal(job.outputs[0].width, 640);
|
||||
assert.equal(job.outputs[0].height, 480);
|
||||
assert.equal(job.appearance, "workbench");
|
||||
assert.deepEqual(job.graphics, { detail: 2, resolutionScale: 3 });
|
||||
assert.deepEqual(job.render, { sizeProfile: "simple" });
|
||||
assert.deepEqual(job.implicitParameters, { radius: 24 });
|
||||
});
|
||||
|
||||
test("shortcut accepts JSON camera objects", async () => {
|
||||
const options = parseSnapshotArgs([
|
||||
"--input",
|
||||
"models/implicit-cad/orb.implicit.js",
|
||||
"--output",
|
||||
"tmp/orb.png",
|
||||
"--camera",
|
||||
"{\"position\":[4,-5,3],\"target\":[0,0,0],\"up\":[0,0,1],\"zoom\":1.4}",
|
||||
]);
|
||||
|
||||
const job = await loadJobFromOptions(options, { stdin: ttyStdin, cwd: process.cwd() });
|
||||
|
||||
assert.deepEqual(job.outputs[0].camera, {
|
||||
position: [4, -5, 3],
|
||||
target: [0, 0, 0],
|
||||
up: [0, 0, 1],
|
||||
zoom: 1.4,
|
||||
});
|
||||
});
|
||||
|
||||
test("GIF shortcut defaults to orbit mode", async () => {
|
||||
const options = parseSnapshotArgs([
|
||||
"--input",
|
||||
"models/implicit-cad/orb.implicit.js",
|
||||
"--output",
|
||||
"tmp/orb.gif",
|
||||
]);
|
||||
|
||||
const job = await loadJobFromOptions(options, { stdin: ttyStdin, cwd: process.cwd() });
|
||||
|
||||
assert.equal(job.mode, "orbit");
|
||||
assert.equal(job.outputs[0].path, "tmp/orb.gif");
|
||||
});
|
||||
|
||||
test("render job derives asset root from input parent", () => withTempImplicitModel(({ root, models }) => {
|
||||
const packet = resolveRenderJobPacket({
|
||||
input: "models/implicit-cad/orb.implicit.js",
|
||||
outputs: [{ path: "tmp/orb.png", camera: "iso" }],
|
||||
}, {
|
||||
cwd: root,
|
||||
timestamp: "20260527T163012Z",
|
||||
});
|
||||
|
||||
const job = packet.jobs[0];
|
||||
assert.equal(packet.single, true);
|
||||
assert.equal(job.resolved.kind, "implicit");
|
||||
assert.equal(job.resolved.rootPath, models);
|
||||
assert.match(job.resolved.inputUrl, /^\/__render_asset\/orb\.implicit\.js\?v=/u);
|
||||
assert.equal(job.appearance, "workbench");
|
||||
assert.deepEqual(job.render, {});
|
||||
assert.deepEqual(job.graphics, {});
|
||||
assert.equal(job.outputs[0].width, 1600);
|
||||
assert.equal(job.outputs[0].height, 1200);
|
||||
assert.equal(path.basename(job.outputs[0].path), "orb_20260527T163012Z.png");
|
||||
}));
|
||||
|
||||
test("render job packet supports multi-output review snapshots", () => withTempImplicitModel(({ root }) => {
|
||||
const packet = resolveRenderJobPacket({
|
||||
input: "models/implicit-cad/orb.implicit.js",
|
||||
render: { frameMargin: 1.55 },
|
||||
outputs: [
|
||||
{ path: "tmp/orb-iso.png", camera: "iso" },
|
||||
{ path: "tmp/orb-front.png", camera: "front", width: 900, height: 700 },
|
||||
{ path: "tmp/orb-top.png", camera: "top" },
|
||||
],
|
||||
}, {
|
||||
cwd: root,
|
||||
timestamp: "20260527T163012Z",
|
||||
});
|
||||
|
||||
const job = packet.jobs[0];
|
||||
assert.equal(packet.single, true);
|
||||
assert.equal(job.outputs.length, 3);
|
||||
assert.equal(job.render.frameMargin, 1.55);
|
||||
assert.equal(job.outputs[0].camera, "iso");
|
||||
assert.equal(job.outputs[1].camera, "front");
|
||||
assert.equal(job.outputs[1].width, 900);
|
||||
assert.equal(job.outputs[1].height, 700);
|
||||
assert.equal(job.outputs[2].camera, "top");
|
||||
assert.deepEqual(
|
||||
job.outputs.map((output) => path.basename(output.path)),
|
||||
[
|
||||
"orb-iso_20260527T163012Z.png",
|
||||
"orb-front_20260527T163012Z.png",
|
||||
"orb-top_20260527T163012Z.png",
|
||||
]
|
||||
);
|
||||
}));
|
||||
|
||||
test("render job packet accepts arrays with one shared timestamp", () => withTempImplicitModel(({ root }) => {
|
||||
const packet = resolveRenderJobPacket([
|
||||
{ input: "models/implicit-cad/orb.implicit.js", outputs: [{ path: "tmp/a.png" }] },
|
||||
{ input: "models/implicit-cad/orb.implicit.js", outputs: [{ path: "tmp/b.png" }] },
|
||||
], {
|
||||
cwd: root,
|
||||
timestamp: "20260527T163012Z",
|
||||
});
|
||||
|
||||
assert.equal(packet.single, false);
|
||||
assert.equal(packet.jobs.length, 2);
|
||||
assert.equal(path.basename(packet.jobs[0].outputs[0].path), "a_20260527T163012Z.png");
|
||||
assert.equal(path.basename(packet.jobs[1].outputs[0].path), "b_20260527T163012Z.png");
|
||||
assert.equal(packet.jobs[0].resolved.inputUrl, packet.jobs[1].resolved.inputUrl);
|
||||
}));
|
||||
|
||||
test("render job accepts animation mode", () => withTempImplicitModel(({ root }) => {
|
||||
const packet = resolveRenderJobPacket({
|
||||
input: "models/implicit-cad/orb.implicit.js",
|
||||
mode: "animate",
|
||||
outputs: [{ path: "tmp/orb.gif" }],
|
||||
}, { cwd: root });
|
||||
|
||||
assert.equal(packet.jobs[0].mode, "animate");
|
||||
}));
|
||||
|
||||
test("non implicit input files are rejected", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "implicitjs-snapshot-"));
|
||||
try {
|
||||
const models = path.join(root, "models");
|
||||
fs.mkdirSync(models, { recursive: true });
|
||||
fs.writeFileSync(path.join(models, "part.js"), "export default {};\n", "utf8");
|
||||
|
||||
assert.throws(
|
||||
() => resolveRenderJobPacket({ input: "models/part.js", outputs: [{ path: "tmp/part.png" }] }, { cwd: root }),
|
||||
/only \.implicit\.js and \.implicit\.mjs/u
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("legacy top-level theme and params fields are rejected", () => withTempImplicitModel(({ root }) => {
|
||||
assert.throws(
|
||||
() => resolveRenderJobPacket({
|
||||
input: "models/implicit-cad/orb.implicit.js",
|
||||
theme: "dark",
|
||||
outputs: [{ path: "tmp/orb.png" }],
|
||||
}, { cwd: root }),
|
||||
/use appearance/u
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveRenderJobPacket({
|
||||
input: "models/implicit-cad/orb.implicit.js",
|
||||
params: {},
|
||||
outputs: [{ path: "tmp/orb.png" }],
|
||||
}, { cwd: root }),
|
||||
/use implicitParameters/u
|
||||
);
|
||||
}));
|
||||
|
||||
test("timestamp output path preserves extension", () => {
|
||||
assert.equal(
|
||||
timestampOutputPath("snapshots/review.png", "20260527T163012Z"),
|
||||
"snapshots/review_20260527T163012Z.png"
|
||||
);
|
||||
});
|
||||
|
||||
test("snapshot routes are package-owned and self-contained", () => {
|
||||
assert.equal(resolveSnapshotRouteFile(`${SNAPSHOT_ORIGIN}/render.html`), RENDER_HTML_PATH);
|
||||
assert.equal(
|
||||
resolveSnapshotRouteFile(`${SNAPSHOT_ORIGIN}/src/common/implicitHeadlessRenderEntry.js`),
|
||||
path.join(PACKAGE_ROOT, "src", "common", "implicitHeadlessRenderEntry.js")
|
||||
);
|
||||
assert.equal(
|
||||
resolveSnapshotRouteFile(`${SNAPSHOT_ORIGIN}/src/common/implicitHeadlessRenderEntry.js?v=test`),
|
||||
path.join(PACKAGE_ROOT, "src", "common", "implicitHeadlessRenderEntry.js")
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveSnapshotRouteFile(`${SNAPSHOT_ORIGIN}/__render_asset/orb.implicit.js`),
|
||||
SnapshotError
|
||||
);
|
||||
});
|
||||
|
||||
test("snapshot renderer does not force Chromium single-process mode", () => {
|
||||
assert.notEqual(chromiumLaunchOptions().args?.includes("--single-process"), true);
|
||||
});
|
||||
|
||||
test("render result printing surfaces captured page warnings", () => {
|
||||
const lines = [];
|
||||
const stdout = { write: (text) => lines.push(text) };
|
||||
|
||||
printRenderResult({
|
||||
ok: true,
|
||||
mode: "view",
|
||||
outputs: [{ path: "tmp/orb.png" }],
|
||||
warnings: ["page console error: THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false"],
|
||||
}, { jsonOutput: false, stdout });
|
||||
|
||||
assert.equal(lines[0], "saved snapshot: tmp/orb.png\n");
|
||||
assert.match(lines[1], /^snapshot warning: page console error: THREE\.WebGLProgram/);
|
||||
});
|
||||
Reference in New Issue
Block a user