Files
openmaic/OpenMAIC/e2e/tests/recent-video-thumbnail.spec.ts
2026-08-16 14:58:47 +08:00

253 lines
8.7 KiB
TypeScript

import type { Page } from '@playwright/test';
import { test, expect } from '../fixtures/base';
import { defaultTheme } from '../fixtures/test-data/scene-content';
const TEST_STAGE_ID = 'e2e-video-thumbnail-stage';
const VIDEO_MEDIA_REF = 'gen_vid_thumbnail';
const LEGACY_STAGE_ID = 'e2e-legacy-video-ref-stage';
const LEGACY_VIDEO_REF = 'gen_vid_1';
const UNIQUE_VIDEO_REF = 'gen_vid_unique_legacy';
const FAILED_EXACT_STAGE_ID = 'e2e-failed-exact-video-ref-stage';
const OTHER_VIDEO_REF = 'gen_vid_other_success';
const POSTER_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=';
async function seedVideoThumbnailStage({
page,
stageId = TEST_STAGE_ID,
courseName = 'Video Thumbnail Course',
slideMediaRef = VIDEO_MEDIA_REF,
storedMediaRef = slideMediaRef,
storedError,
extraStoredMediaRefs = [],
}: {
page: Page;
stageId?: string;
courseName?: string;
slideMediaRef?: string;
storedMediaRef?: string;
storedError?: string;
extraStoredMediaRefs?: string[];
}) {
await page.goto('/', { waitUntil: 'networkidle' });
await page.evaluate(
({
stageId,
courseName,
slideMediaRef,
storedMediaRef,
storedError,
extraStoredMediaRefs,
posterBase64,
theme,
}) => {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open('MAIC-Database');
request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const tx = db.transaction(['mediaFiles'], 'readwrite');
const now = Date.now();
const videoBytes = new Uint8Array([
0, 0, 0, 24, 102, 116, 121, 112, 109, 112, 52, 50, 0, 0, 0, 0, 109, 112, 52, 50, 105,
115, 111, 109,
]);
const posterBytes = Uint8Array.from(atob(posterBase64), (char) => char.charCodeAt(0));
const videoBlob = new Blob([videoBytes], { type: 'video/mp4' });
const posterBlob = new Blob([posterBytes], { type: 'image/png' });
const failedVideoBlob = new Blob([], { type: 'video/mp4' });
const putVideoRecord = (mediaRef: string, error?: string) => {
const blob = error ? failedVideoBlob : videoBlob;
tx.objectStore('mediaFiles').put({
id: `${stageId}:${mediaRef}`,
stageId,
type: 'video',
blob,
mimeType: 'video/mp4',
size: blob.size,
poster: error ? undefined : posterBlob,
prompt: 'A generated classroom video preview',
params: '{}',
error,
createdAt: now,
});
};
const documentScene = {
id: 'scene-video-thumbnail',
stageId,
type: 'slide',
title: 'Video preview',
order: 0,
content: {
type: 'slide',
canvas: {
id: 'slide-video-thumbnail',
viewportSize: 1000,
viewportRatio: 0.5625,
theme,
background: { type: 'solid', color: '#111827' },
elements: [
{
id: 'video-el',
type: 'video',
src: slideMediaRef,
mediaRef: slideMediaRef,
left: 0,
top: 0,
width: 1000,
height: 562.5,
rotate: 0,
autoplay: false,
},
],
},
},
createdAt: now,
updatedAt: now,
};
putVideoRecord(storedMediaRef, storedError);
for (const mediaRef of extraStoredMediaRefs) {
putVideoRecord(mediaRef);
}
tx.oncomplete = () => {
db.close();
const documentRequest = indexedDB.open('maic-documents', 1);
documentRequest.onupgradeneeded = () => {
const documentDb = documentRequest.result;
documentDb.createObjectStore('stages', { keyPath: 'id' });
const scenes = documentDb.createObjectStore('scenes', {
keyPath: ['stageId', 'id'],
});
scenes.createIndex('by-stage', 'stageId');
documentDb.createObjectStore('outlines', { keyPath: 'stageId' });
};
documentRequest.onsuccess = () => {
const documentDb = documentRequest.result;
const documentTx = documentDb.transaction(
['stages', 'scenes', 'outlines'],
'readwrite',
);
documentTx.objectStore('stages').put({
id: stageId,
name: courseName,
description: '',
language: 'en-US',
style: 'professional',
createdAt: now,
updatedAt: now,
dslVersion: '0.1.0',
});
documentTx.objectStore('scenes').put(documentScene);
documentTx.objectStore('outlines').put({
stageId,
outline: { outlines: [], createdAt: now, updatedAt: now },
});
documentTx.oncomplete = () => {
documentDb.close();
resolve();
};
documentTx.onerror = () => reject(documentTx.error);
};
documentRequest.onerror = () => reject(documentRequest.error);
};
tx.onerror = () => reject(tx.error);
};
request.onerror = () => reject(request.error);
});
},
{
stageId,
courseName,
slideMediaRef,
storedMediaRef,
storedError,
extraStoredMediaRefs,
posterBase64: POSTER_BASE64,
theme: defaultTheme,
},
);
await page.goto('/', { waitUntil: 'networkidle' });
}
test.describe('Home recent video thumbnails', () => {
test('renders generated video thumbnails and opens the card from the preview area', async ({
page,
}) => {
await seedVideoThumbnailStage({ page });
const card = page.locator('.group.cursor-pointer').filter({
hasText: 'Video Thumbnail Course',
});
const video = card.locator('[data-video-element] video');
await expect(video).toBeVisible({ timeout: 10_000 });
await expect(video).toHaveAttribute('src', /^blob:/);
await expect(video).toHaveAttribute('poster', /^blob:/);
await expect(video).not.toHaveAttribute('controls', '');
await expect(card.locator('[data-testid="thumbnail-video-indicator"]')).toBeVisible();
await card.click({ position: { x: 24, y: 24 } });
await page.waitForURL(`**/classroom/${TEST_STAGE_ID}`);
const classroomVideo = page.locator('[data-video-element] video[controls]');
await expect(classroomVideo).toHaveCount(1);
await expect(classroomVideo).toBeVisible({ timeout: 10_000 });
await expect(classroomVideo).toHaveAttribute('src', /^blob:/);
});
test('falls back from legacy gen_vid_1 refs to the single stored video media file', async ({
page,
}) => {
await seedVideoThumbnailStage({
page,
stageId: LEGACY_STAGE_ID,
courseName: 'Legacy Video Ref Course',
slideMediaRef: LEGACY_VIDEO_REF,
storedMediaRef: UNIQUE_VIDEO_REF,
});
const card = page.locator('.group.cursor-pointer').filter({
hasText: 'Legacy Video Ref Course',
});
const thumbnailVideo = card.locator('[data-video-element] video');
await expect(thumbnailVideo).toBeVisible({ timeout: 10_000 });
await expect(thumbnailVideo).toHaveAttribute('src', /^blob:/);
await expect(card.locator('[data-testid="thumbnail-video-indicator"]')).toBeVisible();
await card.click({ position: { x: 24, y: 24 } });
await page.waitForURL(`**/classroom/${LEGACY_STAGE_ID}`);
const classroomVideo = page.locator('[data-video-element] video[controls]');
await expect(classroomVideo).toHaveCount(1);
await expect(classroomVideo).toBeVisible({ timeout: 10_000 });
await expect(classroomVideo).toHaveAttribute('src', /^blob:/);
});
test('does not fall back to another video when the exact legacy ref failed', async ({ page }) => {
await seedVideoThumbnailStage({
page,
stageId: FAILED_EXACT_STAGE_ID,
courseName: 'Failed Exact Video Ref Course',
slideMediaRef: LEGACY_VIDEO_REF,
storedMediaRef: LEGACY_VIDEO_REF,
storedError: 'Generation failed',
extraStoredMediaRefs: [OTHER_VIDEO_REF],
});
const card = page.locator('.group.cursor-pointer').filter({
hasText: 'Failed Exact Video Ref Course',
});
await expect(card.locator('[data-testid="thumbnail-video-indicator"]')).toBeVisible();
await expect(card.locator('[data-video-element] video')).toHaveCount(0);
});
});