| import { describe, test, expect } from 'vitest'; |
| import { rewriteAudioRefsToIds, actionsToManifest } from '@/lib/export/classroom-zip-utils'; |
| import { |
| CLASSROOM_ZIP_FORMAT_VERSION, |
| type ClassroomManifest, |
| } from '@/lib/export/classroom-zip-types'; |
| import type { SpeechAction, SpotlightAction } from '@/lib/types/action'; |
|
|
| |
|
|
| describe('rewriteAudioRefsToIds', () => { |
| test('replaces audioRef with new audioId in speech actions', () => { |
| const actions = [ |
| { id: 'a1', type: 'speech' as const, text: 'Hello', audioRef: 'audio/abc.mp3' }, |
| { id: 'a2', type: 'spotlight' as const, elementId: 'el1' }, |
| ]; |
| const audioRefMap = { 'audio/abc.mp3': 'new-audio-id-1' }; |
| const result = rewriteAudioRefsToIds(actions, audioRefMap); |
| expect(result[0]).toMatchObject({ |
| type: 'speech', |
| text: 'Hello', |
| audioId: 'new-audio-id-1', |
| }); |
| expect(result[1]).toMatchObject({ type: 'spotlight', elementId: 'el1' }); |
| }); |
|
|
| test('skips speech actions without audioRef', () => { |
| const actions = [ |
| { id: 'a1', type: 'speech' as const, text: 'Hello', audioUrl: 'https://example.com/a.mp3' }, |
| ]; |
| const result = rewriteAudioRefsToIds(actions, {}); |
| expect(result[0]).toMatchObject({ |
| type: 'speech', |
| text: 'Hello', |
| audioUrl: 'https://example.com/a.mp3', |
| }); |
| }); |
| }); |
|
|
| |
|
|
| describe('actionsToManifest', () => { |
| test('converts audioId to audioRef for speech actions', () => { |
| const actions = [ |
| { |
| id: 'act1', |
| type: 'speech' as const, |
| text: 'Hello', |
| audioId: 'audio-123', |
| voice: 'alloy', |
| speed: 1, |
| } as SpeechAction, |
| { id: 'act2', type: 'spotlight' as const, elementId: 'el1' } as SpotlightAction, |
| ]; |
| const audioIdToPath = new Map([['audio-123', 'audio/audio-123.mp3']]); |
|
|
| const result = actionsToManifest(actions, audioIdToPath); |
|
|
| expect(result[0]).toMatchObject({ |
| type: 'speech', |
| text: 'Hello', |
| audioRef: 'audio/audio-123.mp3', |
| voice: 'alloy', |
| }); |
| expect(result[0]).not.toHaveProperty('audioId'); |
| expect(result[1]).toMatchObject({ type: 'spotlight', elementId: 'el1' }); |
| }); |
|
|
| test('preserves audioUrl when audioId is absent', () => { |
| const actions = [ |
| { |
| id: 'act1', |
| type: 'speech' as const, |
| text: 'Hi', |
| audioUrl: 'https://cdn.example.com/hi.mp3', |
| } as SpeechAction, |
| ]; |
| const result = actionsToManifest(actions, new Map()); |
| expect(result[0]).toMatchObject({ |
| type: 'speech', |
| text: 'Hi', |
| audioUrl: 'https://cdn.example.com/hi.mp3', |
| }); |
| expect(result[0]).not.toHaveProperty('audioRef'); |
| }); |
| }); |
|
|
| |
|
|
| describe('manifest round-trip', () => { |
| test('manifest structure is valid JSON-serializable', () => { |
| const manifest: ClassroomManifest = { |
| formatVersion: CLASSROOM_ZIP_FORMAT_VERSION, |
| exportedAt: new Date().toISOString(), |
| appVersion: '0.1.0', |
| stage: { |
| name: 'Test Course', |
| description: 'A test', |
| language: 'en-US', |
| style: 'professional', |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| }, |
| agents: [ |
| { |
| name: 'Prof', |
| role: 'lecturer', |
| persona: 'Friendly professor', |
| avatar: 'π¨βπ«', |
| color: '#4A90D9', |
| priority: 1, |
| }, |
| ], |
| scenes: [ |
| { |
| type: 'slide', |
| title: 'Intro', |
| order: 0, |
| |
| content: { type: 'slide', canvas: { id: 's1', elements: [] } } as any, |
| |
| actions: [{ id: 'a1', type: 'speech', text: 'Welcome', audioRef: 'audio/a1.mp3' } as any], |
| }, |
| ], |
| mediaIndex: { |
| 'audio/a1.mp3': { type: 'audio', format: 'mp3', duration: 5.2 }, |
| }, |
| }; |
|
|
| const serialized = JSON.stringify(manifest); |
| const deserialized = JSON.parse(serialized) as ClassroomManifest; |
|
|
| expect(deserialized.formatVersion).toBe(CLASSROOM_ZIP_FORMAT_VERSION); |
| expect(deserialized.stage.name).toBe('Test Course'); |
| expect(deserialized.agents).toHaveLength(1); |
| expect(deserialized.scenes).toHaveLength(1); |
| expect(deserialized.scenes[0].actions?.[0]).toMatchObject({ |
| type: 'speech', |
| audioRef: 'audio/a1.mp3', |
| }); |
| expect(deserialized.mediaIndex['audio/a1.mp3']).toMatchObject({ |
| type: 'audio', |
| duration: 5.2, |
| }); |
| }); |
| }); |
|
|