83 lines
5.7 KiB
TypeScript
83 lines
5.7 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { normalizeStoryScriptRows } from "@/lib/video-canvas/story-script";
|
|
import { validateVideoReferences } from "@/lib/video-canvas/reference-input";
|
|
import { preserveGeneratedVideo } from "@/lib/video-canvas/archive-result";
|
|
|
|
test("three requested shots retain their content and durations without cloned filler", () => {
|
|
const rows = [{ sceneDescription: "杯子全景", duration: "3s" }, { sceneDescription: "杯柄特写", duration: "2s" }, { sceneDescription: "咖啡液面", duration: "3s" }];
|
|
const result = normalizeStoryScriptRows(rows);
|
|
expect(result).toHaveLength(3);
|
|
expect(result.map(row => row.sceneDescription)).toEqual(rows.map(row => row.sceneDescription));
|
|
expect(result.map(row => row.duration)).toEqual(["3s", "2s", "3s"]);
|
|
});
|
|
test("empty model rows do not create invented shots", () => {
|
|
expect(normalizeStoryScriptRows([null, {}, { shot: "01", duration: "3s" }, [], ["", ""], ["01"], ["01", "3s"]])).toEqual([]);
|
|
});
|
|
test("same first and last frame survive while only probing the URL once", async () => {
|
|
const seen: string[] = [];
|
|
const images = ["https://example.com/one.png", "https://example.com/one.png"];
|
|
const out = await validateVideoReferences(images, async url => { seen.push(url); return "reachable"; });
|
|
expect(out.images).toEqual(images);
|
|
expect(seen).toHaveLength(1);
|
|
});
|
|
test("invalid references never silently turn into text-only generation", async () => {
|
|
for (const input of [["not a url"], [null], "https://example.com/a.png", ["https://user:pass@example.com/a.png"]]) {
|
|
let calls = 0;
|
|
expect((await validateVideoReferences(input, async () => { calls++; return "reachable"; })).status).toBe(400);
|
|
expect(calls).toBe(0);
|
|
}
|
|
});
|
|
test("temporary network errors are distinguished from dead images", async () => {
|
|
const images = ["https://example.com/a.png"];
|
|
expect((await validateVideoReferences(images, async () => "unknown")).status).toBe(503);
|
|
expect((await validateVideoReferences(images, async () => { throw new Error("network"); })).status).toBe(503);
|
|
expect((await validateVideoReferences(images, async () => "unreachable")).status).toBe(400);
|
|
});
|
|
test("archive success replaces URL and preserves generation metadata", async () => {
|
|
const out = await preserveGeneratedVideo("https://provider.example/video", { duration: 8, model: "veo" }, async () => "https://storage.example/file.mp4");
|
|
expect(out).toMatchObject({ url: "https://storage.example/file.mp4", metadata: { storageStatus: "archived", duration: 8, model: "veo", temporaryVideoUrl: "https://provider.example/video" } });
|
|
});
|
|
test("archive failure preserves the generated output and cannot mark it permanently saved", async () => {
|
|
for (const archive of [async () => { throw new Error("storage unavailable"); }, async (url: string) => url]) {
|
|
const out = await preserveGeneratedVideo("https://provider.example/video", { permanentVideoUrl: "stale" }, archive);
|
|
expect(out.url).toBe("https://provider.example/video");
|
|
expect(out.metadata.storageStatus).toBe("temporary");
|
|
expect(out.metadata.permanentVideoUrl).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
import { canvasGenerationCount, canvasImageRequest, canvasImageResultUrls } from '@/lib/video-canvas/generation-contract';
|
|
import { angleRequestParameters, anglePresets } from '@/lib/video-canvas/angle-settings';
|
|
import { fetchCanvasDownloadAsset } from '@/lib/canvas/photoshop-safe-download';
|
|
|
|
test('image composer transmits resolution and all references without injecting demo content',()=>{
|
|
for(const resolution of ['1K','2K','4K']) {
|
|
const request=canvasImageRequest({model:'configured-image',ratio:'9:16',resolution,count:'4张'},[]);
|
|
expect(request).toMatchObject({model:'configured-image',aspectRatio:'9:16',imageSize:resolution,imageCount:4,referenceImages:[]});
|
|
expect(request.referenceImage).toBeUndefined();
|
|
}
|
|
expect(canvasImageRequest({ratio:'1:1',resolution:'2K',count:'2'},['https://a.test/1','https://a.test/1','https://a.test/2'])).toMatchObject({imageCount:2,referenceImage:'https://a.test/1',referenceImages:['https://a.test/2']});
|
|
});
|
|
test('all returned image results survive, while failures never expose fallback images',()=>{
|
|
const data={images:[{originalUrl:'a',thumbnailUrl:'thumb'},{originalUrl:'b'},{thumbnailUrl:'c'},{}]};
|
|
expect(canvasImageResultUrls({success:true,data})).toEqual(['a','b','c']);
|
|
expect(canvasImageResultUrls({success:false,data})).toEqual([]);
|
|
expect(['1个','2个','4个','Infinity','100','0'].map(canvasGenerationCount)).toEqual([1,2,4,1,1,1]);
|
|
});
|
|
test('angle controls convert degrees and percent zoom into provider units',()=>{
|
|
expect(angleRequestParameters({horizontalAngle:-90,verticalAngle:-45,zoom:50})).toEqual({horizontalAngle:270,verticalAngle:-30,zoom:5});
|
|
expect(angleRequestParameters(anglePresets['全景俯拍'])).toEqual({horizontalAngle:0,verticalAngle:90,zoom:0});
|
|
expect(anglePresets['鱼眼视角'].prompt).toContain('鱼眼');
|
|
expect(()=>angleRequestParameters({horizontalAngle:NaN,verticalAngle:0,zoom:50})).toThrow();
|
|
});
|
|
test('audio downloads preserve original bytes and extension instead of passing through an image decoder',async()=>{
|
|
const original=globalThis.fetch;
|
|
const bytes=Buffer.from('RIFF00000000WAVEfmt synthetic QA audio');
|
|
globalThis.fetch=async()=>new Response(bytes,{headers:{'content-type':'audio/wav'}});
|
|
try {
|
|
const result=await fetchCanvasDownloadAsset({file:{url:'https://cdn.example/audio.wav',name:'配音'},index:0,requestOrigin:'https://canvas.example',cookie:'',retryCount:1});
|
|
expect(result.ok).toBe(true);
|
|
if(result.ok){expect(result.bytes).toEqual(bytes);expect(result.filename).toMatch(/\.wav$/);expect(result.contentType).toBe('audio/wav');}
|
|
} finally {globalThis.fetch=original;}
|
|
});
|