304 lines
7.8 KiB
TypeScript
304 lines
7.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { PPTVideoElement, Slide } from '@openmaic/dsl';
|
|
import type { MediaTask } from '@/lib/store/media-generation';
|
|
import {
|
|
resolveSlideMedia,
|
|
resolveSlideMediaState,
|
|
} from '@/components/slide-renderer/use-resolved-slide';
|
|
|
|
const slide: Slide = {
|
|
id: 'slide-1',
|
|
viewportSize: 1000,
|
|
viewportRatio: 0.5625,
|
|
background: { type: 'solid', color: '#ffffff' },
|
|
theme: {
|
|
fontName: 'Arial',
|
|
fontColor: '#111111',
|
|
backgroundColor: '#ffffff',
|
|
themeColors: ['#111111'],
|
|
},
|
|
elements: [
|
|
{
|
|
id: 'video-1',
|
|
type: 'video',
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 56,
|
|
rotate: 0,
|
|
src: 'gen_vid_1',
|
|
autoplay: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
function task(overrides: Partial<MediaTask>): MediaTask {
|
|
return {
|
|
elementId: 'gen_vid_1',
|
|
type: 'video',
|
|
status: 'pending',
|
|
prompt: '',
|
|
params: {},
|
|
retryCount: 0,
|
|
stageId: 'stage-1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('resolveSlideMedia', () => {
|
|
it('clears an unresolved generated image while status metadata stays separate', () => {
|
|
const imageSlide: Slide = {
|
|
...slide,
|
|
elements: [
|
|
{
|
|
id: 'image-1',
|
|
type: 'image',
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100,
|
|
rotate: 0,
|
|
fixedRatio: true,
|
|
src: 'gen_img_1',
|
|
},
|
|
],
|
|
};
|
|
|
|
const resolved = resolveSlideMedia(imageSlide, 'stage-1', {
|
|
gen_img_1: {
|
|
...task({ elementId: 'gen_img_1', type: 'image', status: 'pending' }),
|
|
},
|
|
});
|
|
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
type: 'image',
|
|
src: '',
|
|
});
|
|
});
|
|
|
|
it('does not hand a pending generated video source to the DOM', () => {
|
|
const resolved = resolveSlideMedia(slide, 'stage-1', {
|
|
gen_vid_1: task({ status: 'pending' }),
|
|
});
|
|
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
type: 'video',
|
|
src: '',
|
|
});
|
|
});
|
|
|
|
it('replaces generated video placeholders with done object URLs and posters', () => {
|
|
const resolved = resolveSlideMedia(slide, 'stage-1', {
|
|
gen_vid_1: task({ status: 'done', objectUrl: 'blob:video', poster: 'blob:poster' }),
|
|
});
|
|
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
type: 'video',
|
|
src: 'blob:video',
|
|
poster: 'blob:poster',
|
|
});
|
|
});
|
|
|
|
it('uses the shared lease once it settles after a task completes', () => {
|
|
const resolved = resolveSlideMedia(
|
|
slide,
|
|
'stage-1',
|
|
{
|
|
gen_vid_1: task({ status: 'done', objectUrl: 'blob:task-video' }),
|
|
},
|
|
{ assetUrls: { gen_vid_1: 'blob:pool-video' } },
|
|
);
|
|
|
|
expect(resolved.elements[0]).toMatchObject({ src: 'blob:pool-video' });
|
|
});
|
|
|
|
it('keeps a pending task hidden even when stale pool bytes exist', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const withPoster: Slide = {
|
|
...slide,
|
|
elements: [{ ...baseVideo, poster: 'ast_poster_ref' }],
|
|
};
|
|
const resolved = resolveSlideMedia(
|
|
withPoster,
|
|
'stage-1',
|
|
{ gen_vid_1: task({ status: 'pending', objectUrl: undefined }) },
|
|
{
|
|
assetUrls: {
|
|
gen_vid_1: 'blob:pool-video',
|
|
ast_poster_ref: 'blob:pool-poster',
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
src: '',
|
|
poster: 'blob:pool-poster',
|
|
});
|
|
});
|
|
|
|
it('shows last-good pool bytes after a failed regeneration', () => {
|
|
const resolved = resolveSlideMedia(
|
|
slide,
|
|
'stage-1',
|
|
{ gen_vid_1: task({ status: 'failed', objectUrl: undefined }) },
|
|
{ assetUrls: { gen_vid_1: 'blob:pool-video' } },
|
|
);
|
|
|
|
expect(resolved.elements[0]).toMatchObject({ src: 'blob:pool-video' });
|
|
});
|
|
|
|
it('uses a pool URL when an allocated ref has no task after reload', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const allocatedSlide: Slide = {
|
|
...slide,
|
|
elements: [
|
|
{
|
|
...baseVideo,
|
|
src: 'ast_video_ref',
|
|
mediaRef: 'ast_video_ref',
|
|
poster: 'ast_poster_ref',
|
|
},
|
|
],
|
|
};
|
|
|
|
const resolved = resolveSlideMedia(
|
|
allocatedSlide,
|
|
'stage-1',
|
|
{},
|
|
{
|
|
assetUrls: {
|
|
ast_video_ref: 'blob:pool-video',
|
|
ast_poster_ref: 'blob:pool-poster',
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
src: 'blob:pool-video',
|
|
poster: 'blob:pool-poster',
|
|
});
|
|
});
|
|
|
|
it('resolves an allocated background through the same lease boundary', () => {
|
|
const backgroundSlide: Slide = {
|
|
...slide,
|
|
background: { type: 'image', image: { src: 'ast_background', size: 'cover' } },
|
|
};
|
|
|
|
const resolved = resolveSlideMediaState(
|
|
backgroundSlide,
|
|
'stage-1',
|
|
{},
|
|
{
|
|
assetUrls: { ast_background: 'blob:pool-background' },
|
|
},
|
|
);
|
|
|
|
expect(resolved.slide.background).toEqual({
|
|
type: 'image',
|
|
image: { src: 'blob:pool-background', size: 'cover' },
|
|
});
|
|
expect(resolved.backgroundResolution).toEqual({
|
|
kind: 'url',
|
|
url: 'blob:pool-background',
|
|
});
|
|
});
|
|
|
|
it('resolves an allocated poster even when the video source is already a direct URL', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const directVideo: Slide = {
|
|
...slide,
|
|
elements: [
|
|
{
|
|
...baseVideo,
|
|
src: 'https://example.test/video.mp4',
|
|
poster: 'ast_poster_ref',
|
|
},
|
|
],
|
|
};
|
|
|
|
const resolved = resolveSlideMedia(
|
|
directVideo,
|
|
'stage-1',
|
|
{},
|
|
{
|
|
assetUrls: { ast_poster_ref: 'blob:pool-poster' },
|
|
},
|
|
);
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
src: 'https://example.test/video.mp4',
|
|
poster: 'blob:pool-poster',
|
|
});
|
|
});
|
|
|
|
it('preserves a hydrated video URL when an opaque mediaRef has no stage context', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const hydratedSlide: Slide = {
|
|
...slide,
|
|
elements: [
|
|
{
|
|
...baseVideo,
|
|
src: 'blob:thumbnail-video',
|
|
mediaRef: 'gen_vid_1',
|
|
},
|
|
],
|
|
};
|
|
|
|
const resolved = resolveSlideMediaState(hydratedSlide, undefined, {});
|
|
|
|
expect(resolved.slide.elements[0]).toMatchObject({
|
|
src: 'blob:thumbnail-video',
|
|
mediaRef: 'gen_vid_1',
|
|
});
|
|
expect(resolved.byElementId['video-1']).toMatchObject({
|
|
ref: 'gen_vid_1',
|
|
resolution: { kind: 'raw', value: 'blob:thumbnail-video' },
|
|
});
|
|
});
|
|
|
|
it('hides an allocated ref when pool resolution misses', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const allocatedSlide: Slide = {
|
|
...slide,
|
|
elements: [
|
|
{
|
|
...baseVideo,
|
|
src: 'ast_missing_ref',
|
|
mediaRef: 'ast_missing_ref',
|
|
},
|
|
],
|
|
};
|
|
|
|
const resolved = resolveSlideMedia(allocatedSlide, 'stage-1', {}, { assetUrls: {} });
|
|
expect(resolved.elements[0]).toMatchObject({
|
|
src: '',
|
|
mediaRef: 'ast_missing_ref',
|
|
});
|
|
});
|
|
|
|
it('reports disabled instead of pending when generation is off', () => {
|
|
const resolved = resolveSlideMediaState(
|
|
slide,
|
|
'stage-1',
|
|
{},
|
|
{
|
|
videoGenerationDisabled: true,
|
|
},
|
|
);
|
|
|
|
expect(resolved.slide.elements[0]).toMatchObject({ src: '' });
|
|
expect(resolved.byElementId['video-1'].resolution).toEqual({ kind: 'disabled' });
|
|
});
|
|
|
|
it('keeps a bare relative source when task and pool lookup both miss', () => {
|
|
const baseVideo = slide.elements[0] as PPTVideoElement;
|
|
const unknownSlide: Slide = {
|
|
...slide,
|
|
elements: [{ ...baseVideo, src: 'logo.mp4' }],
|
|
};
|
|
|
|
const resolved = resolveSlideMedia(unknownSlide, 'stage-1', {}, { assetUrls: {} });
|
|
expect(resolved.elements[0]).toMatchObject({ src: 'logo.mp4' });
|
|
});
|
|
});
|