11 changed files with 422 additions and 14 deletions
@ -0,0 +1,204 @@ |
|||
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|||
import { externalizeWorkflowMedia, hydrateWorkflowMedia } from "../mediaStorage"; |
|||
import type { WorkflowFile } from "@/store/workflowStore"; |
|||
|
|||
const browserFsMocks = vi.hoisted(() => ({ |
|||
writeBrowserGenerationFile: vi.fn(), |
|||
loadBrowserGenerationFile: vi.fn(), |
|||
})); |
|||
|
|||
vi.mock("../browserFileSystem", () => ({ |
|||
isBrowserFileSystemPath: (path: string | null | undefined) => |
|||
typeof path === "string" && path.startsWith("browserfs://"), |
|||
joinBrowserFileSystemPath: (basePath: string, folderName: string) => |
|||
`${basePath.replace(/\/+$/, "")}/${encodeURIComponent(folderName)}`, |
|||
writeBrowserGenerationFile: browserFsMocks.writeBrowserGenerationFile, |
|||
loadBrowserGenerationFile: browserFsMocks.loadBrowserGenerationFile, |
|||
})); |
|||
|
|||
const imageData = "data:image/png;base64,aW1hZ2U="; |
|||
const videoData = "data:video/mp4;base64,dmlkZW8="; |
|||
const audioData = "data:audio/mpeg;base64,YXVkaW8="; |
|||
const workflowPath = "browserfs://root/project"; |
|||
|
|||
function makeWorkflow(): WorkflowFile { |
|||
return { |
|||
version: 1, |
|||
id: "wf-browserfs", |
|||
name: "browserfs media", |
|||
nodes: [ |
|||
{ |
|||
id: "imageInput-1", |
|||
type: "imageInput", |
|||
position: { x: 0, y: 0 }, |
|||
data: { |
|||
image: imageData, |
|||
filename: "input.png", |
|||
dimensions: null, |
|||
}, |
|||
}, |
|||
{ |
|||
id: "generateVideo-1", |
|||
type: "generateVideo", |
|||
position: { x: 200, y: 0 }, |
|||
data: { |
|||
inputImages: [imageData], |
|||
inputPrompt: "move", |
|||
outputVideo: videoData, |
|||
outputVideoRef: undefined, |
|||
outputVideoRemoteUrl: undefined, |
|||
outputVideoStorageStatus: undefined, |
|||
selectedVideoHistoryIndex: 0, |
|||
videoHistory: [ |
|||
{ |
|||
id: "vid-history-1", |
|||
video: videoData, |
|||
timestamp: 1, |
|||
prompt: "move", |
|||
}, |
|||
], |
|||
status: "completed", |
|||
error: null, |
|||
}, |
|||
}, |
|||
{ |
|||
id: "generateAudio-1", |
|||
type: "generateAudio", |
|||
position: { x: 400, y: 0 }, |
|||
data: { |
|||
inputPrompt: "sound", |
|||
outputAudio: audioData, |
|||
outputAudioRef: undefined, |
|||
selectedAudioHistoryIndex: 0, |
|||
audioHistory: [ |
|||
{ |
|||
id: "aud-history-1", |
|||
audio: audioData, |
|||
timestamp: 1, |
|||
prompt: "sound", |
|||
}, |
|||
], |
|||
status: "completed", |
|||
error: null, |
|||
}, |
|||
}, |
|||
], |
|||
edges: [], |
|||
edgeStyle: "angular", |
|||
} as WorkflowFile; |
|||
} |
|||
|
|||
describe("mediaStorage browserfs support", () => { |
|||
beforeEach(() => { |
|||
browserFsMocks.writeBrowserGenerationFile.mockReset(); |
|||
browserFsMocks.loadBrowserGenerationFile.mockReset(); |
|||
browserFsMocks.writeBrowserGenerationFile.mockResolvedValue({ filePath: "browserfs://root/project/media.bin" }); |
|||
vi.stubGlobal("fetch", vi.fn(async () => ({ |
|||
json: async () => ({ success: true, imageId: "server-ref" }), |
|||
}))); |
|||
}); |
|||
|
|||
it("externalizes browserfs media through browser file helpers instead of API fetch", async () => { |
|||
const workflow = makeWorkflow(); |
|||
|
|||
const externalized = await externalizeWorkflowMedia(workflow, workflowPath); |
|||
|
|||
expect(fetch).not.toHaveBeenCalled(); |
|||
expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith( |
|||
"browserfs://root/project/inputs", |
|||
expect.any(String), |
|||
imageData |
|||
); |
|||
expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith( |
|||
"browserfs://root/project/generations", |
|||
"vid-history-1", |
|||
videoData |
|||
); |
|||
expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith( |
|||
"browserfs://root/project/generations", |
|||
"aud-history-1", |
|||
audioData |
|||
); |
|||
|
|||
expect(externalized.nodes[0].data).toMatchObject({ |
|||
image: null, |
|||
imageRef: expect.any(String), |
|||
}); |
|||
expect(externalized.nodes[1].data).toMatchObject({ |
|||
inputImages: [], |
|||
inputImageRefs: [expect.any(String)], |
|||
outputVideo: null, |
|||
outputVideoRef: "vid-history-1", |
|||
outputVideoStorageStatus: "localized", |
|||
videoHistory: [ |
|||
{ |
|||
id: "vid-history-1", |
|||
timestamp: 1, |
|||
prompt: "move", |
|||
}, |
|||
], |
|||
}); |
|||
expect(externalized.nodes[2].data).toMatchObject({ |
|||
outputAudio: null, |
|||
outputAudioRef: "aud-history-1", |
|||
}); |
|||
}); |
|||
|
|||
it("hydrates browserfs media refs through browser file helpers instead of API fetch", async () => { |
|||
browserFsMocks.loadBrowserGenerationFile.mockImplementation(async (directoryPath: string, mediaId: string) => { |
|||
if (directoryPath.endsWith("/inputs") && mediaId === "input-ref") return imageData; |
|||
if (directoryPath.endsWith("/generations") && mediaId === "video-ref") return videoData; |
|||
if (directoryPath.endsWith("/generations") && mediaId === "audio-ref") return audioData; |
|||
return null; |
|||
}); |
|||
|
|||
const workflow: WorkflowFile = { |
|||
...makeWorkflow(), |
|||
nodes: [ |
|||
{ |
|||
id: "imageInput-1", |
|||
type: "imageInput", |
|||
position: { x: 0, y: 0 }, |
|||
data: { image: null, imageRef: "input-ref" }, |
|||
}, |
|||
{ |
|||
id: "generateVideo-1", |
|||
type: "generateVideo", |
|||
position: { x: 200, y: 0 }, |
|||
data: { |
|||
inputImages: [], |
|||
inputImageRefs: ["input-ref"], |
|||
outputVideo: null, |
|||
outputVideoRef: "video-ref", |
|||
outputVideoStorageStatus: "localized", |
|||
status: "idle", |
|||
error: null, |
|||
}, |
|||
}, |
|||
{ |
|||
id: "generateAudio-1", |
|||
type: "generateAudio", |
|||
position: { x: 400, y: 0 }, |
|||
data: { |
|||
inputPrompt: "sound", |
|||
outputAudio: null, |
|||
outputAudioRef: "audio-ref", |
|||
status: "idle", |
|||
error: null, |
|||
}, |
|||
}, |
|||
], |
|||
} as WorkflowFile; |
|||
|
|||
const hydrated = await hydrateWorkflowMedia(workflow, workflowPath); |
|||
|
|||
expect(fetch).not.toHaveBeenCalled(); |
|||
expect(hydrated.nodes[0].data).toMatchObject({ image: imageData }); |
|||
expect(hydrated.nodes[1].data).toMatchObject({ |
|||
inputImages: [imageData], |
|||
outputVideo: videoData, |
|||
outputVideoStorageStatus: "localized", |
|||
}); |
|||
expect(hydrated.nodes[2].data).toMatchObject({ outputAudio: audioData }); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue