Files
openmaic/OpenMAIC/tests/slide-renderer/use-resolved-image-src.test.ts
2026-08-16 14:58:47 +08:00

158 lines
5.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type { PPTImageElement } from '@openmaic/dsl';
import type { MediaTask } from '@/lib/store/media-generation';
import { resolveImageSrc } from '@/components/slide-renderer/components/element/ImageElement/useResolvedImageSrc';
const STAGE = 'stage-a';
const PLACEHOLDER: PPTImageElement = {
id: 'el-placeholder',
type: 'image',
src: 'gen_img_alpha_001',
left: 0,
top: 0,
width: 100,
height: 100,
rotate: 0,
fixedRatio: false,
};
const CONCRETE: PPTImageElement = {
...PLACEHOLDER,
id: 'el-concrete',
src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
};
function task(over: Partial<MediaTask>): MediaTask {
return {
elementId: PLACEHOLDER.src,
type: 'image',
status: 'done',
prompt: '',
params: {} as MediaTask['params'],
retryCount: 0,
stageId: STAGE,
...over,
};
}
describe('resolveImageSrc (pure)', () => {
it('returns the objectUrl when the placeholder task is done', () => {
const r = resolveImageSrc(
PLACEHOLDER,
STAGE,
task({ status: 'done', objectUrl: 'blob:fake-1' }),
);
expect(r.resolvedSrc).toBe('blob:fake-1');
expect(r.isPlaceholder).toBe(true);
expect(r.task?.status).toBe('done');
});
it('never returns a raw generated placeholder when no task is supplied', () => {
const r = resolveImageSrc(PLACEHOLDER, STAGE, undefined);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'placeholder' });
expect(r.isPlaceholder).toBe(true);
expect(r.task).toBeUndefined();
});
it.each(['pending', 'generating'] as const)('keeps task status %s non-renderable', (status) => {
const r = resolveImageSrc(PLACEHOLDER, STAGE, task({ status, objectUrl: undefined }));
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'pending' });
expect(r.task?.status).toBe(status);
});
it.each(['pending', 'generating'] as const)(
'keeps a %s task authoritative over stale pool bytes',
(status) => {
const r = resolveImageSrc(
PLACEHOLDER,
STAGE,
task({ status, objectUrl: undefined }),
'blob:pool-image',
);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'pending' });
},
);
it('renders last-good pool bytes for a failed regeneration', () => {
const r = resolveImageSrc(
PLACEHOLDER,
STAGE,
task({ status: 'failed', objectUrl: undefined }),
'blob:pool-image',
);
expect(r.resolvedSrc).toBe('blob:pool-image');
expect(r.resolution).toEqual({ kind: 'url', url: 'blob:pool-image', retryable: true });
});
it('returns failed without bytes so the surface can render error UI', () => {
const r = resolveImageSrc(PLACEHOLDER, STAGE, task({ status: 'failed', objectUrl: undefined }));
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'failed', retryable: true });
});
it('falls back when a done task has no objectUrl set', () => {
const r = resolveImageSrc(PLACEHOLDER, STAGE, task({ status: 'done', objectUrl: undefined }));
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'pending' });
});
it('cross-stage isolation: drops a done task that belongs to a different stage', () => {
const r = resolveImageSrc(
PLACEHOLDER,
STAGE,
task({ status: 'done', objectUrl: 'blob:other-stage', stageId: 'stage-other' }),
);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'placeholder' });
expect(r.task).toBeUndefined();
});
it('does not consider anything a placeholder when there is no stageId', () => {
// Even if a "done" task is supplied, no stageId → skip placeholder logic entirely.
const r = resolveImageSrc(
PLACEHOLDER,
undefined,
task({ status: 'done', objectUrl: 'blob:leak' }),
);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'placeholder' });
expect(r.isPlaceholder).toBe(false);
expect(r.task).toBeUndefined();
});
it('passes through non-placeholder src unchanged (additive contract)', () => {
// Even with a done task supplied — the regex gate rejects non-placeholder src.
const r = resolveImageSrc(
CONCRETE,
STAGE,
task({ status: 'done', objectUrl: 'blob:should-not-touch' }),
);
expect(r.resolvedSrc).toBe(CONCRETE.src);
expect(r.isPlaceholder).toBe(false);
expect(r.task).toBeUndefined();
});
it('falls back to an unknown browser-resolvable string when pool lookup misses', () => {
const unknown = { ...PLACEHOLDER, src: 'logo.png' };
const r = resolveImageSrc(unknown, STAGE, undefined, null);
expect(r.resolvedSrc).toBe('logo.png');
});
it('hides an opaque allocated ref when pool lookup misses', () => {
const allocated = { ...PLACEHOLDER, src: 'ast_missing_image' };
const r = resolveImageSrc(allocated, STAGE, undefined, null);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'placeholder' });
});
it('returns disabled for an unresolved placeholder when generation is off', () => {
const r = resolveImageSrc(PLACEHOLDER, STAGE, undefined, null, true);
expect(r.resolvedSrc).toBe('');
expect(r.resolution).toEqual({ kind: 'disabled' });
});
});