150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { modelSupportsReferenceImageInput } from "@/lib/ai/reference-image-capability";
|
|
|
|
function readSource(path: string) {
|
|
return readFileSync(resolve(process.cwd(), path), "utf8");
|
|
}
|
|
|
|
test.describe("3D workspace and Style Pro regressions", () => {
|
|
test("opens World splats in first-person mode without the debug HUD", () => {
|
|
const page = readSource("app/canvas/[id]/model3d/page.tsx");
|
|
const viewer = readSource(
|
|
"components/canvas/world3d/World3DViewer.tsx"
|
|
);
|
|
|
|
expect(page).toContain(
|
|
'workspaceMode === "world" && worldViewportMode === "splat"'
|
|
);
|
|
expect(page).toContain("showInfoOverlay={false}");
|
|
expect(viewer).toContain("showInfoOverlay = !minimalFrame");
|
|
expect(viewer).toContain("showInfoOverlay={showInfoOverlay}");
|
|
});
|
|
|
|
test("keeps each GLB viewer material instance isolated and texture-aware", () => {
|
|
const viewer = readSource(
|
|
"components/canvas/model3d/Model3DViewer.tsx"
|
|
);
|
|
|
|
expect(viewer).toContain("cloneSceneWithIndependentMaterials");
|
|
expect(viewer).toContain("const clonedMaterial = material.clone()");
|
|
expect(viewer).toContain("typedSourceTexture.clone()");
|
|
expect(viewer).toContain("clonedMaterialRecord[key] = clonedTexture");
|
|
expect(viewer).toContain("disposedTextures.add(texture)");
|
|
expect(viewer).toContain("texture.dispose()");
|
|
expect(viewer).toContain("<primitive dispose={null}");
|
|
expect(viewer).toContain(
|
|
"configureMaterialTextures(clonedMaterial, node.geometry)"
|
|
);
|
|
});
|
|
|
|
test("loads Qiniu-hosted GLB assets through the authenticated same-origin proxy", () => {
|
|
const assetUrl = readSource("lib/canvas/three-asset-url.ts");
|
|
const proxy = readSource("app/api/canvas/3d/model-proxy/route.ts");
|
|
|
|
for (const host of [
|
|
"yybb.top",
|
|
"qiniucs.com",
|
|
"qiniudn.com",
|
|
"clouddn.com",
|
|
"qbox.me",
|
|
]) {
|
|
expect(assetUrl).toContain(`\"${host}\"`);
|
|
expect(proxy).toContain(`\"${host}\"`);
|
|
}
|
|
});
|
|
|
|
test("does not block a completed Tripo task while permanent assets are archived", () => {
|
|
const route = readSource("app/api/canvas/3d/tasks/[taskId]/route.ts");
|
|
const page = readSource("app/canvas/[id]/model3d/page.tsx");
|
|
const imageNode = readSource("components/canvas/nodes/ImageNode.tsx");
|
|
|
|
expect(route).toContain("return { url: sourceUrl, pending: true }");
|
|
expect(route).toContain('String(taskStatus).trim().toLowerCase() === "success" && assetsPending');
|
|
expect(route).toContain('? "running"');
|
|
expect(route).toContain("assetsPending,");
|
|
expect(page).toContain("pollFailureStreakRef.current < 5");
|
|
expect(page).toContain("cancelled || pollInFlight");
|
|
expect(page).toContain("restoreRecoverableTaskForPolling");
|
|
expect(imageNode).toContain("CANVAS_3D_MAX_CONSECUTIVE_QUERY_FAILURES");
|
|
});
|
|
|
|
test("uses the same hidden-model access rule in Style Pro as canvas", () => {
|
|
const route = readSource("app/api/style-pro/model-options/route.ts");
|
|
const canvasModelsRoute = readSource("app/(chat)/api/models/route.ts");
|
|
|
|
expect(route).toContain("getUserById");
|
|
expect(route).toContain("canAccessHiddenAiModels");
|
|
expect(route).toContain("databaseUserRole: currentUser?.role");
|
|
expect(route).toContain("includeHidden: allowHiddenModels");
|
|
expect(route).toContain('"Cache-Control": "private, no-store, max-age=0"');
|
|
expect(canvasModelsRoute).toContain("canAccessHiddenAiModels");
|
|
expect(canvasModelsRoute).toContain("databaseUserRole: currentUser?.role");
|
|
});
|
|
|
|
test("refreshes Style Pro models after an administrator changes the catalog", () => {
|
|
const workspace = readSource(
|
|
"components/style-pro/StyleProWorkspace.tsx"
|
|
);
|
|
|
|
expect(workspace).toContain("refreshInterval: 30_000");
|
|
expect(workspace).toContain("revalidateOnFocus: true");
|
|
expect(workspace).toContain("revalidateOnReconnect: true");
|
|
});
|
|
|
|
test("enforces Style Pro model availability on both display and execution", () => {
|
|
const options = readSource("lib/style-pro/model-options.ts");
|
|
const availability = readSource(
|
|
"lib/style-pro/model-availability.ts"
|
|
);
|
|
const referenceCapability = readSource(
|
|
"lib/ai/reference-image-capability.ts"
|
|
);
|
|
|
|
expect(options).toContain(".filter(isStyleProModelRunnable)");
|
|
expect(options).toContain("getStyleProModelAvailability(entry)");
|
|
expect(availability).toContain("modelSupportsReferenceImageInput");
|
|
expect(availability).toContain('availabilityLabel: "仅文生图"');
|
|
expect(availability).toContain('availabilityLabel: "暂不可用"');
|
|
expect(referenceCapability).toContain("GEMINI_TEXT_ONLY_IMAGE_MODEL_REGEX");
|
|
|
|
const task = readSource("lib/style-pro/task.ts");
|
|
expect(task).toContain("const runnableModels = await getStyleProImageModelEntries");
|
|
expect(task).toContain("当前不支持参考图,请重新选择");
|
|
});
|
|
|
|
test("keeps known text-only Gemini image aliases out of reference-image tools", () => {
|
|
expect(
|
|
modelSupportsReferenceImageInput(
|
|
{
|
|
modelId: "nano-banana-pro",
|
|
displayName: "BanA Pro",
|
|
supportsVision: true,
|
|
},
|
|
{ type: "gemini" }
|
|
)
|
|
).toBe(false);
|
|
expect(
|
|
modelSupportsReferenceImageInput(
|
|
{
|
|
modelId: "gemini-3-pro-image-preview",
|
|
displayName: "BanA Pro V",
|
|
supportsVision: true,
|
|
},
|
|
{ type: "bananaimage" }
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
test("opens the canvas conversation panel by default", () => {
|
|
const page = readSource(
|
|
"app/canvas/[id]/CanvasEditorLegacyPageClient.tsx"
|
|
);
|
|
|
|
expect(page).toContain(
|
|
"const [isPanelOpen, setIsPanelOpen] = useState(true)"
|
|
);
|
|
});
|
|
});
|