253 lines
8.7 KiB
TypeScript
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);
|
|
});
|
|
});
|